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

  1. Run `but agentlog sync`, then retry the capture/publish
  2. Re-capture the session under the current `but` version so `:turns` is rewritten in List form
  3. Align all machines/hooks on one `but` build
  4. 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

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


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/040469a08a2b5dfe. Report an issue: GitHub.