Summary
resolve_model_switch_method() in crates/buzz-acp/src/acp.rs reads the configId key from each session/new configOptions entry and skips any entry that lacks it. The claude-agent-acp adapter (v0.61.0) keys those entries with id, not configId. Every model-category entry is therefore skipped, the desired model never matches, and the session silently falls back to the CLI default from the user's ~/.claude/settings.json.
Reproduction
Buzz Desktop 0.4.24, macOS arm64, claude-agent-acp 0.61.0, Claude Code CLI 2.1.219.
- Set a Claude Code agent's model to Opus in Buzz Desktop (per-agent or global default).
- Restart the agent.
buzz-acp is launched with BUZZ_ACP_MODEL=opus[1m] as expected.
- Ask the agent what model it is running. It reports whatever
~/.claude/settings.json pins (in my case claude-fable-5[1m]), not Opus.
Harness log shows the fallback:
WARN pool::model: desired model opus[1m] not found in agent's available models - proceeding with agent default
The pool::model tracing target is not written to the per-agent log files, so nothing user-visible records the miss.
Root cause
The adapter's actual session/new response (captured with a stdio JSON-RPC probe against the bundled adapter):
{
"configOptions": [{
"id": "model",
"name": "Model",
"category": "model",
"type": "select",
"currentValue": "claude-fable-5[1m]",
"options": [
{ "value": "default" },
{ "value": "opus[1m]" },
{ "value": "claude-fable-5[1m]" },
{ "value": "sonnet" },
{ "value": "haiku" }
]
}],
"models": null
}
Note the key is id. But resolve_model_switch_method() does:
let config_id = match config_opt.get("configId").and_then(|v| v.as_str()) {
Some(id) => id,
None => continue, // claude-agent-acp entries always take this branch
};
So opus[1m] is a valid option value sitting right there in the response, and the exact-match loop never sees it. The unstable models path cannot rescue it either because the adapter returns models: null.
The unit tests all use configId-keyed fixtures, which is why they pass despite the drift.
The set side is unaffected: the ACP SDK's SetSessionConfigOptionRequest schema takes configId as the request param, and the adapter's setSessionConfigOption handler looks entries up by params.configId against o.id. Only the read side in buzz-acp is wrong.
Relationship to existing issues and PRs
Fix
Accept both spellings when extracting the config id:
config_opt.get("configId").or_else(|| config_opt.get("id")).and_then(|v| v.as_str())
PR incoming with this change plus a regression test using an id-keyed payload that mirrors the real adapter response.
Summary
resolve_model_switch_method()incrates/buzz-acp/src/acp.rsreads theconfigIdkey from eachsession/newconfigOptionsentry and skips any entry that lacks it. Theclaude-agent-acpadapter (v0.61.0) keys those entries withid, notconfigId. Every model-category entry is therefore skipped, the desired model never matches, and the session silently falls back to the CLI default from the user's~/.claude/settings.json.Reproduction
Buzz Desktop 0.4.24, macOS arm64,
claude-agent-acp0.61.0, Claude Code CLI 2.1.219.buzz-acpis launched withBUZZ_ACP_MODEL=opus[1m]as expected.~/.claude/settings.jsonpins (in my caseclaude-fable-5[1m]), not Opus.Harness log shows the fallback:
The
pool::modeltracing target is not written to the per-agent log files, so nothing user-visible records the miss.Root cause
The adapter's actual
session/newresponse (captured with a stdio JSON-RPC probe against the bundled adapter):{ "configOptions": [{ "id": "model", "name": "Model", "category": "model", "type": "select", "currentValue": "claude-fable-5[1m]", "options": [ { "value": "default" }, { "value": "opus[1m]" }, { "value": "claude-fable-5[1m]" }, { "value": "sonnet" }, { "value": "haiku" } ] }], "models": null }Note the key is
id. Butresolve_model_switch_method()does:So
opus[1m]is a valid option value sitting right there in the response, and the exact-match loop never sees it. The unstablemodelspath cannot rescue it either because the adapter returnsmodels: null.The unit tests all use
configId-keyed fixtures, which is why they pass despite the drift.The set side is unaffected: the ACP SDK's
SetSessionConfigOptionRequestschema takesconfigIdas the request param, and the adapter'ssetSessionConfigOptionhandler looks entries up byparams.configIdagainsto.id. Only the read side in buzz-acp is wrong.Relationship to existing issues and PRs
session/newresponse, the lookup just reads the wrong key.ANTHROPIC_MODELat spawn from the desktop side. That masks the symptom for spawn-time selection but leaves the ACP config-option switch path broken, including for any future mid-session switching (feat(agents): allow changing an agent's model mid-session (like /model in Claude Code) #2543).Fix
Accept both spellings when extracting the config id:
PR incoming with this change plus a regression test using an
id-keyed payload that mirrors the real adapter response.