gitbutlerapp/gitbutler · error
existing GitMeta key '{turns_key}' is not a list
Error message
existing GitMeta key '{turns_key}' is not a list What it means
When appending new capture records, the write path first reads the existing `<session_prefix>:turns` value to merge turn summaries; `None` is fine (new session), a List is parsed, and any other variant bails. So this error occurs on the write/append side, meaning the previously stored turns entry has a shape the current writer cannot merge.
Source
Thrown at crates/but-agentlog/src/gitmeta/write.rs:81
let handle = gitmeta.target(&target);
let publication_status = capture_publication_status(repo_path, &handle, session_key)?;
let session_prefix = session_storage_prefix(publication_status, session_key);
let sources_key = format!("{session_prefix}:sources");
let source_prefix = format!("{session_prefix}:source:{source_key}");
let transcript_key = format!("{session_prefix}:transcript");
let record_hashes_key = format!("{session_prefix}:record-hashes");
let turns_key = format!("{session_prefix}:turns");
let associated_targets_key = format!("{session_prefix}:associated-targets");
let turns_value = handle
.get_value(&turns_key)
.with_context(|| format!("failed to read GitMeta key '{turns_key}'"))?;
let previous_turns = match turns_value.as_ref() {
None => Vec::new(),
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) {View on GitHub (pinned to caf1f223d3)
Solutions
- Run `but agentlog sync`, then retry the capture/publish
- Re-capture the session under the current `but` version so `:turns` is rewritten in List form
- Align all machines/hooks on one `but` build
- Restore the last List-valued `:turns` from GitMeta ref history, or delete the key so the writer starts a fresh turn list
Example fix
// before
let previous_turns = match turns_value.as_ref() {
None => Vec::new(),
Some(MetaValue::List(entries)) => stored_turn_summary_entries(entries.to_vec(), &turns_key)?,
Some(_) => bail!("existing GitMeta key '{turns_key}' is not a list"),
};
// after: quarantine a malformed turns list instead of blocking capture
let previous_turns = match turns_value.as_ref() {
None => Vec::new(),
Some(MetaValue::List(entries)) => stored_turn_summary_entries(entries.to_vec(), &turns_key)?,
Some(_) => {
warn!("turns key '{turns_key}' malformed; starting a fresh turn list");
Vec::new()
}
}; Defensive patterns
Strategy: validation
Validate before calling
// Before appending records, validate the existing turns entry's shape
match handle.get_value(&turns_key)? {
None | Some(MetaValue::List(_)) => { /* safe to append */ }
Some(_) => { eprintln!("existing turns key malformed; sync or re-capture first"); }
} Type guard
fn appendable_turns(v: Option<&MetaValue>) -> bool {
matches!(v, None | Some(MetaValue::List(_)))
} Try / catch
match append_records(&handle, &records) {
Ok(()) => {}
Err(err) if err.to_string().contains("turns") && err.to_string().contains("not a list") => {
eprintln!("session turns malformed; run sync, then re-capture")
}
Err(err) => return Err(err),
} Prevention
- Keep hook and CLI on the same but build as the machine that created the session
- Never downgrade but below the version that wrote a session
- After a failed append, run sync before retrying capture
When it happens
Trigger: The agentlog capture hook or publish appending records to a session whose existing `:turns` key holds a non-List value — e.g. the session was last written by an incompatible but-agentlog version or the key was corrupted.
Common situations: Downgrading or mixing `but` versions on the same repository/metadata remote; GitMeta conflict resolution that reshaped the value; sessions written by pre-release builds; manual key tampering.
Related errors
- existing GitMeta key '{turns_key}' is not a list
- existing GitMeta key '{record_hashes_key}' is not a set
- 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
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/040469a08a2b5dfe.
Report an issue: GitHub.