gitbutlerapp/gitbutler · error

existing GitMeta key '{associated_targets_key}' is not a str

Error message

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

What it means

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.

Source

Thrown at crates/but-agentlog/src/gitmeta/read.rs:263

fn session_associations_observe_target(
    handle: &SessionTargetHandle<'_>,
    session_key: &str,
    status: PublicationStatus,
    target: RelatedTarget<'_>,
) -> Result<bool> {
    let associated_targets_key = format!(
        "{}:associated-targets",
        session_storage_prefix(status, session_key)
    );
    let Some(value) = handle
        .get_value(&associated_targets_key)
        .with_context(|| format!("failed to read GitMeta key '{associated_targets_key}'"))?
    else {
        return Ok(false);
    };
    let MetaValue::String(value) = value else {
        bail!("existing GitMeta key '{associated_targets_key}' is not a string");
    };
    let targets: StoredSessionAssociations = serde_json::from_str(&value).with_context(|| {
        format!("existing GitMeta key '{associated_targets_key}' has invalid JSON")
    })?;
    Ok(targets.observes(target))
}

#[derive(Deserialize)]
struct StoredSessionAssociations {
    #[serde(default)]
    branches: BTreeSet<String>,
    #[serde(default)]
    reviews: BTreeSet<String>,
    #[serde(default)]
    changes: BTreeSet<String>,
}

impl StoredSessionAssociations {

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Run `but agentlog sync` to fetch consistent state, then retry
  2. Re-capture or re-publish the session (hook + publish) so the associations key is rewritten in the current string format
  3. Align `but` versions across machines that sync the same GitMeta remote
  4. As a last resort, delete the malformed `:associated-targets` key — the read path treats absence as 'observes nothing'

Example fix

// before
let observes = session_observes(&handle, status, session_key, target)?;

// after: absence and malformed both mean 'no association'
let observes = match session_observes(&handle, status, session_key, target) {
    Ok(v) => v,
    Err(err) if err.to_string().contains("associated-targets") => false,
    Err(err) => return Err(err),
};
Defensive patterns

Strategy: type-guard

Validate before calling

// Verify the associations key parses before relying on related-session checks
let key = format!("{session_prefix}:associated-targets");
if let Some(MetaValue::String(raw)) = handle.get_value(&key)? {
    serde_json::from_str::<StoredSessionAssociations>(&raw)?; // fail early with context
}

Type guard

fn associations_are_valid(v: Option<&MetaValue>) -> bool {
    match v {
        Some(MetaValue::String(s)) => serde_json::from_str::<StoredSessionAssociations>(s).is_ok(),
        _ => false,
    }
}

Try / catch

match session_observes_target(&handle, status, session_key, target) {
    Ok(v) => v,
    Err(err) if err.to_string().contains("associated-targets") => false,
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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