{"record":{"id":"10e60c56e1891ad2","repo":"gitbutlerapp/gitbutler","slug":"existing-gitmeta-key-associated-targets-key-is","errorCode":null,"errorMessage":"existing GitMeta key '{associated_targets_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/read.rs","lineNumber":263,"sourceCode":"\nfn session_associations_observe_target(\n    handle: &SessionTargetHandle<'_>,\n    session_key: &str,\n    status: PublicationStatus,\n    target: RelatedTarget<'_>,\n) -> Result<bool> {\n    let associated_targets_key = format!(\n        \"{}:associated-targets\",\n        session_storage_prefix(status, session_key)\n    );\n    let Some(value) = handle\n        .get_value(&associated_targets_key)\n        .with_context(|| format!(\"failed to read GitMeta key '{associated_targets_key}'\"))?\n    else {\n        return Ok(false);\n    };\n    let MetaValue::String(value) = value else {\n        bail!(\"existing GitMeta key '{associated_targets_key}' is not a string\");\n    };\n    let targets: StoredSessionAssociations = serde_json::from_str(&value).with_context(|| {\n        format!(\"existing GitMeta key '{associated_targets_key}' has invalid JSON\")\n    })?;\n    Ok(targets.observes(target))\n}\n\n#[derive(Deserialize)]\nstruct StoredSessionAssociations {\n    #[serde(default)]\n    branches: BTreeSet<String>,\n    #[serde(default)]\n    reviews: BTreeSet<String>,\n    #[serde(default)]\n    changes: BTreeSet<String>,\n}\n\nimpl StoredSessionAssociations {","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-agentlog/src/gitmeta/read.rs#L245-L281","documentation":"Each session stores its observed branch/review/change associations as a JSON blob in the `:associated-targets` key, which must be a `MetaValue::String` deserializable into `StoredSessionAssociations`. The read path that decides whether a session observes a target bails when the key exists but is not a String. This is a shape check on stored JSON-in-a-string data.","triggerScenarios":"Related-session lookups and skim outlines calling `turn_detail_observes_target`/association checks for `gitbutler:agent-session:<status>:<key>:associated-targets` when that key holds a Set or List variant instead of the serialized JSON string.","commonSituations":"Writer version skew changing how associations are stored; a session partially migrated between formats; GitMeta merge/resolution producing a non-string value; external tooling overwriting the key.","solutions":["Run `but agentlog sync` to fetch consistent state, then retry","Re-capture or re-publish the session (hook + publish) so the associations key is rewritten in the current string format","Align `but` versions across machines that sync the same GitMeta remote","As a last resort, delete the malformed `:associated-targets` key — the read path treats absence as 'observes nothing'"],"exampleFix":"// before\nlet observes = session_observes(&handle, status, session_key, target)?;\n\n// after: absence and malformed both mean 'no association'\nlet observes = match session_observes(&handle, status, session_key, target) {\n    Ok(v) => v,\n    Err(err) if err.to_string().contains(\"associated-targets\") => false,\n    Err(err) => return Err(err),\n};","handlingStrategy":"type-guard","validationCode":"// Verify the associations key parses before relying on related-session checks\nlet key = format!(\"{session_prefix}:associated-targets\");\nif let Some(MetaValue::String(raw)) = handle.get_value(&key)? {\n    serde_json::from_str::<StoredSessionAssociations>(&raw)?; // fail early with context\n}","typeGuard":"fn associations_are_valid(v: Option<&MetaValue>) -> bool {\n    match v {\n        Some(MetaValue::String(s)) => serde_json::from_str::<StoredSessionAssociations>(s).is_ok(),\n        _ => false,\n    }\n}","tryCatchPattern":"match session_observes_target(&handle, status, session_key, target) {\n    Ok(v) => v,\n    Err(err) if err.to_string().contains(\"associated-targets\") => false,\n    Err(err) => return Err(err),\n}","preventionTips":["Keep association data written only via publish/hook flows","Re-publish sessions after version upgrades","Do not delete individual association fields from GitMeta by hand"],"tags":["gitmeta","type-mismatch","associated-targets","json","rust"],"backgroundTag":"stored-data-type-mismatch","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}