{"record":{"id":"c0e7fb21cd86c653","repo":"gitbutlerapp/gitbutler","slug":"existing-gitmeta-key-detail-key-is-not-a-strin-c0e7fb","errorCode":null,"errorMessage":"existing GitMeta key '{detail_key}' is not a string","messagePattern":"existing GitMeta key '(.+?)' is not a string","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-agentlog/src/gitmeta/write.rs","lineNumber":705,"sourceCode":"            format!(\"existing GitMeta key '{turns_key}' has invalid turn summary\")\n        })?;\n        if !matches!(\n            summary[\"environment_snapshot_status\"].as_str(),\n            Some(\"failed\" | \"partial\")\n        ) || summary[\"source_key\"].as_str() != Some(source_key)\n        {\n            continue;\n        }\n        let turn_key = summary[\"turn_key\"]\n            .as_str()\n            .context(\"existing turn summary is missing turn_key\")?\n            .to_owned();\n        let detail_key = format!(\"{session_prefix}:turn:{turn_key}\");\n        let Some(MetaValue::String(detail)) = handle\n            .get_value(&detail_key)\n            .with_context(|| format!(\"failed to read GitMeta key '{detail_key}'\"))?\n        else {\n            bail!(\"existing GitMeta key '{detail_key}' is not a string\");\n        };\n        let detail: Value = serde_json::from_str(&detail)\n            .with_context(|| format!(\"existing GitMeta key '{detail_key}' has invalid JSON\"))?;\n        if !detail_records_are_incoming(&detail, &incoming_record_hashes) {\n            continue;\n        }\n        let is_new_best = best\n            .as_ref()\n            .is_none_or(|(timestamp, best_index, _, _, _)| {\n                entry\n                    .timestamp\n                    .cmp(timestamp)\n                    .then_with(|| index.cmp(best_index))\n                    .is_gt()\n            });\n        if is_new_best {\n            best = Some((entry.timestamp, index, summary, turn_key, detail));\n        }","sourceCodeStart":687,"sourceCodeEnd":723,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-agentlog/src/gitmeta/write.rs#L687-L723","documentation":"`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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Run `but agentlog sync` to complete the partial state, then retry capture/publish","Produce one more agent turn in that session and let the hook re-capture — enrichment then works from complete data","Align `but` versions on all writers of that session","Inspect GitMeta history for `gitbutler:agent-session:*:turn:<turn_key>` and restore or delete the stale entry"],"exampleFix":"// before: one arm covers both missing and wrong-type\nlet Some(MetaValue::String(detail)) = handle.get_value(&detail_key)? else {\n    bail!(\"existing GitMeta key '{detail_key}' is not a string\");\n};\n\n// after: distinguish the cases and skip unenrichable turns\nlet detail = match handle.get_value(&detail_key)? {\n    Some(MetaValue::String(detail)) => detail,\n    Some(_) => { warn!(\"turn detail '{detail_key}' has wrong type; skipping\"); continue }\n    None => { warn!(\"turn detail '{detail_key}' missing; skipping\"); continue }\n};","handlingStrategy":"validation","validationCode":"// Before the write path, verify each referenced turn detail key\nlet detail_key = format!(\"{session_prefix}:turn:{turn_key}\");\nmatch handle.get_value(&detail_key)? {\n    Some(MetaValue::String(_)) => { /* enrichable */ }\n    _ => { /* missing or wrong shape; sync or skip enrichment */ }\n}","typeGuard":"fn enrichable_detail(v: Option<&MetaValue>) -> bool {\n    matches!(v, Some(MetaValue::String(_)))\n}","tryCatchPattern":"match enrich_incomplete_turn(&handle, ...) {\n    Ok(Some(turn)) => turns.push(turn),\n    Err(err) if err.to_string().contains(\":turn:\") => {\n        eprintln!(\"turn detail unavailable; continuing without enrichment\")\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Let publishes complete; interrupted writes are the main source of absent detail keys","Sync after failed publishes before capturing again","Align but versions to avoid reshaped detail values"],"tags":["gitmeta","type-mismatch","turn-detail","write-path","torn-write","rust"],"backgroundTag":"stored-data-type-mismatch","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}