gitbutlerapp/gitbutler · error

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

Error message

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

What it means

`optional_string_value` is the projection's generic reader for per-source label keys — `:agent`, `:provider`, `:model`, and `:tool-version` under `gitbutler:agent-session:<key>:source:<source_key>:*`. It returns None for a missing key and Some for a String, but bails if the key exists with any other MetaValue variant. One malformed label key kills the projection for all requested sessions.

Source

Thrown at crates/but-agentlog/src/projection.rs:754

                .then_with(|| lhs.model.cmp(&rhs.model))
        });
        labels.dedup();
        output.insert(session_key.clone(), labels);
    }
    Ok(output)
}

fn optional_string_value(
    handle: &git_meta_lib::SessionTargetHandle<'_>,
    key: &str,
) -> Result<Option<String>> {
    match handle
        .get_value(key)
        .with_context(|| format!("failed to read GitMeta key '{key}'"))?
    {
        None => Ok(None),
        Some(MetaValue::String(value)) => Ok(Some(value)),
        Some(_) => bail!("existing GitMeta key '{key}' is not a string"),
    }
}

fn session_handle(request: &ProjectionRequest, session_key: &str) -> String {
    opaque_handle(
        "ps",
        &[
            &request.pr.github_repository_id.to_string(),
            &request.pr.pull_request.to_string(),
            &request.snapshot.metadata_oid,
            session_key,
        ],
    )
}

fn turn_handle(request: &ProjectionRequest, session_key: &str, turn_key: &str) -> String {
    opaque_handle(
        "pt",

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Run `but agentlog sync` and retry the projection
  2. Re-capture the affected sessions with the current `but` build to rewrite the label keys
  3. Align `but` versions across contributing machines
  4. Identify the malformed key (the error message names it) and restore its last String value from GitMeta history or delete it

Example fix

// before
Some(_) => bail!("existing GitMeta key '{key}' is not a string"),

// after: skip malformed label keys (they are optional by design)
match handle.get_value(key)? {
    None => Ok(None),
    Some(MetaValue::String(value)) => Ok(Some(value)),
    Some(_) => Ok(None),
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Pre-validate optional label keys before running the projection
for key in [":agent", ":provider", ":model", ":tool-version"] {
    if let Some(v) = handle.get_value(&format!("{source_prefix}{key}"))? {
        if !matches!(v, MetaValue::String(_)) { /* skip this label */ }
    }
}

Type guard

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

Try / catch

match optional_string_value(&handle, &label_key) {
    Ok(v) => v,
    Err(err) if err.to_string().contains("is not a string") => None,
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Running a projection over sessions where any `<source_prefix>:agent|:provider|:model|:tool-version` key exists as a Set or List instead of a String — typically written by version-skewed writers or reshaped by conflicted merges.

Common situations: Mixed `but` versions writing source metadata; a GitMeta merge resolution that changed value types; manual edits while debugging source labels.

Related errors


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