gitbutlerapp/gitbutler · error
existing GitMeta key '{record_hashes_key}' is not a set
Error message
existing GitMeta key '{record_hashes_key}' is not a set What it means
The write path deduplicates incoming records against a `<session_prefix>:record-hashes` key that must be a `MetaValue::Set` of already-seen record hashes; `None` means a fresh set. If the key exists with any other variant, appending bails. This stops duplicate records but also stops all capture for that session until the key is fixed.
Source
Thrown at crates/but-agentlog/src/gitmeta/write.rs:96
Some(MetaValue::List(entries)) => {
stored_turn_summary_entries(entries.to_vec(), &turns_key)?
}
Some(_) => bail!("existing GitMeta key '{turns_key}' is not a list"),
};
let previous_turn_key = previous_turns
.last()
.map(|turn| turn.summary.turn_key.to_owned());
let incoming_record_hashes = records
.iter()
.map(|record| record.source_record_hash.clone())
.collect::<Vec<_>>();
let mut seen_hashes = match handle
.get_value(&record_hashes_key)
.with_context(|| format!("failed to read GitMeta key '{record_hashes_key}'"))?
{
None => HashSet::new(),
Some(MetaValue::Set(hashes)) => hashes.into_iter().collect(),
Some(_) => bail!("existing GitMeta key '{record_hashes_key}' is not a set"),
};
records.retain(|record| {
if seen_hashes.contains(&record.source_record_hash) {
false
} else {
seen_hashes.insert(record.source_record_hash.clone());
true
}
});
if records.is_empty() {
let metadata_changed = enrich_incomplete_turn(
&handle,
turns_value,
session_key,
source_key,
&incoming_record_hashes,
publication_status,View on GitHub (pinned to caf1f223d3)
Solutions
- Run `but agentlog sync` and retry the operation
- Re-capture the session with the current `but` build to rewrite `:record-hashes` as a Set
- Align versions across machines and hooks
- Restore the last Set-valued version from GitMeta history, or delete the key (cost: records may be re-stored once until dedup state rebuilds)
Example fix
// before
let mut seen_hashes = match handle.get_value(&record_hashes_key)? {
None => HashSet::new(),
Some(MetaValue::Set(hashes)) => hashes.into_iter().collect(),
Some(_) => bail!("existing GitMeta key '{record_hashes_key}' is not a set"),
};
// after: reset dedup state on a malformed key
let mut seen_hashes = match handle.get_value(&record_hashes_key)? {
Some(MetaValue::Set(hashes)) => hashes.into_iter().collect(),
_ => HashSet::new(),
}; Defensive patterns
Strategy: validation
Validate before calling
// Before publish, confirm the record-hashes key is a Set
if let Some(value) = handle.get_value(&record_hashes_key)? {
if !matches!(value, MetaValue::Set(_)) { /* malformed; sync or delete key */ }
} Type guard
fn dedup_state_is_set(v: Option<&MetaValue>) -> bool {
matches!(v, Some(MetaValue::Set(_)))
} Try / catch
match publish_records(&handle, &records) {
Ok(()) => {}
Err(err) if err.to_string().contains("record-hashes") => {
eprintln!("dedup state malformed; run sync and retry — dedup rebuilds safely")
}
Err(err) => return Err(err),
} Prevention
- Keep record-hash keys owned by but-agentlog only
- Re-run publish after sync; dedup loss only risks one duplicate record, not data loss
- Align but versions across machines writing the same session
When it happens
Trigger: Hook capture or publish appending records to a session whose existing `:record-hashes` key holds a String or List — e.g. written by an incompatible writer version or reshaped by a metadata merge.
Common situations: Mixed `but` versions writing one session; a conflicted GitMeta sync resolved into the wrong shape; manual edits; restoring from an incompatible backup.
Related errors
- existing GitMeta key '{turns_key}' is not a list
- existing GitMeta key '{associated_targets_key}' is not a str
- existing GitMeta key '{detail_key}' is not a string
- existing GitMeta key '{updated_at_key}' is not a string
- existing GitMeta key '{index_key}' is not a set
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/e531a52e5b858495.
Report an issue: GitHub.