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
- Run `but agentlog sync` and retry the projection
- Re-capture the affected sessions with the current `but` build to rewrite the label keys
- Align `but` versions across contributing machines
- 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
- Treat label keys as optional metadata; never require them for pipeline success
- Re-capture sessions after version upgrades to normalize label shapes
- Avoid writing label keys from scripts or editors
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
- existing GitMeta key '{sources_key}' is not a set
- existing GitMeta key '{updated_at_key}' is not a string
- existing GitMeta key '{index_key}' is not a set
- existing GitMeta key '{associated_targets_key}' is not a str
- existing GitMeta key '{turns_key}' is not a list
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/4369f72caf286f07.
Report an issue: GitHub.