gitbutlerapp/gitbutler · error

existing GitMeta key '{detail_key}' is not a string

Error message

existing GitMeta key '{detail_key}' is not a string

What it means

`enrich_incomplete_turn` scans stored turn details for incoming record hashes using `let Some(MetaValue::String(detail)) = ...`; the else branch fires both when `<session_prefix>:turn:<turn_key>` is missing and when it holds a non-String variant, but the message only says 'is not a string'. So this error actually covers 'detail key absent or wrong shape' during the write-path turn-enrichment pass.

Source

Thrown at crates/but-agentlog/src/gitmeta/write.rs:705

            format!("existing GitMeta key '{turns_key}' has invalid turn summary")
        })?;
        if !matches!(
            summary["environment_snapshot_status"].as_str(),
            Some("failed" | "partial")
        ) || summary["source_key"].as_str() != Some(source_key)
        {
            continue;
        }
        let turn_key = summary["turn_key"]
            .as_str()
            .context("existing turn summary is missing turn_key")?
            .to_owned();
        let detail_key = format!("{session_prefix}:turn:{turn_key}");
        let Some(MetaValue::String(detail)) = handle
            .get_value(&detail_key)
            .with_context(|| format!("failed to read GitMeta key '{detail_key}'"))?
        else {
            bail!("existing GitMeta key '{detail_key}' is not a string");
        };
        let detail: Value = serde_json::from_str(&detail)
            .with_context(|| format!("existing GitMeta key '{detail_key}' has invalid JSON"))?;
        if !detail_records_are_incoming(&detail, &incoming_record_hashes) {
            continue;
        }
        let is_new_best = best
            .as_ref()
            .is_none_or(|(timestamp, best_index, _, _, _)| {
                entry
                    .timestamp
                    .cmp(timestamp)
                    .then_with(|| index.cmp(best_index))
                    .is_gt()
            });
        if is_new_best {
            best = Some((entry.timestamp, index, summary, turn_key, detail));
        }

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Run `but agentlog sync` to complete the partial state, then retry capture/publish
  2. Produce one more agent turn in that session and let the hook re-capture — enrichment then works from complete data
  3. Align `but` versions on all writers of that session
  4. Inspect GitMeta history for `gitbutler:agent-session:*:turn:<turn_key>` and restore or delete the stale entry

Example fix

// before: one arm covers both missing and wrong-type
let Some(MetaValue::String(detail)) = handle.get_value(&detail_key)? else {
    bail!("existing GitMeta key '{detail_key}' is not a string");
};

// after: distinguish the cases and skip unenrichable turns
let detail = match handle.get_value(&detail_key)? {
    Some(MetaValue::String(detail)) => detail,
    Some(_) => { warn!("turn detail '{detail_key}' has wrong type; skipping"); continue }
    None => { warn!("turn detail '{detail_key}' missing; skipping"); continue }
};
Defensive patterns

Strategy: validation

Validate before calling

// Before the write path, verify each referenced turn detail key
let detail_key = format!("{session_prefix}:turn:{turn_key}");
match handle.get_value(&detail_key)? {
    Some(MetaValue::String(_)) => { /* enrichable */ }
    _ => { /* missing or wrong shape; sync or skip enrichment */ }
}

Type guard

fn enrichable_detail(v: Option<&MetaValue>) -> bool {
    matches!(v, Some(MetaValue::String(_)))
}

Try / catch

match enrich_incomplete_turn(&handle, ...) {
    Ok(Some(turn)) => turns.push(turn),
    Err(err) if err.to_string().contains(":turn:") => {
        eprintln!("turn detail unavailable; continuing without enrichment")
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: The capture/publish write path enriching an incomplete turn where the `:turn:<turn_key>` key referenced by the stored turns list is absent (torn earlier write, partial sync) or stored as a non-String variant (version skew).

Common situations: An earlier publish was interrupted between writing the turns list and the turn detail keys; mixed `but` versions wrote the session; a conflicted GitMeta merge dropped or reshaped detail keys.

Related errors


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