Live model tracking after set_model (AgentClient.CurrentModel)
Root cause
Section titled “Root cause”docs/design-notes/2026-07-31-history-restore-flow-and-model-picker.md
section 3 documented (and the implementation followed) “the current model”
as client.InitMessage.Model — the resolved model name captured once from
the system/init message at connect time. That was correct as long as
AgentClient.InitMessage was the only place a model could ever be recorded.
It stopped being correct the moment AgentHub.SwitchModel started sending a
live set_model control_request (same note, same section): a successful
set_model genuinely changes which model answers the next turn (R02 section
5.4), but nothing ever touched InitMessage.Model afterwards —
AgentClient.HandleControlResponse only logs failures and raises
ControlRequestResolved(kind, success, error); it never mutates any
“current model” field. Every reader of “the current model”
(StatusBarView.ResolveModelName, HeaderView’s picker button label, its
GenericMenu checkmark, and StatusBarView.RefreshContextMeter’s
currentResolvedModel key for SelectPrimaryModelUsage) reads
InitMessage.Model directly, so all four kept showing the pre-switch model
until the next full reconnect re-ran system/init.
The ctx meter compounds this: AgentHub._lastModelUsage was also left
untouched by SwitchModel (only StartFresh/SwitchToSession cleared it),
so after the next turn completed under the new model, SelectPrimaryModelUsage’s
exact-match branch keyed off the stale currentResolvedModel failed to find
the new model’s modelUsage entry and silently fell back to the
largest-token heuristic — which can pick the wrong entry entirely when a
helper model (e.g. title generation) is also present in the same result.
Options considered
Section titled “Options considered”- Have
set_model’s control_response carry the resolved model name and read it off the response. Rejected: no captured fixture or research note documents theset_modelsuccess response’s payload shape (unlikecan_use_tool/initialize, R02/02b never captured one against a live process). Depending on an unverified wire field would repeat exactly the mistakeAgentClient.cs’s--allowedToolscomment already warns about elsewhere in this file.LiveCliIntegrationTests.SetModel_LiveRoundTriponly assertssuccess/error, not response shape. - Re-run the whole
initializehandshake after everyset_model. Rejected: wasteful (a second full commands/agents/models round trip for a one-field change) andset_modelis documented as a lightweight “keeps running” control request, not a resync point. - Track the requested model value locally and resolve it against the
ALREADY-CACHED
InitializeResponse.Response["models"]array (the samevalue->resolvedModelmappingHeaderView.ParseModelsalready uses to render the picker), committing it as the live model only once the matchingcontrol_responsereports success. Chosen.
Decision
Section titled “Decision”AgentClient gains:
- A private
_liveModelfield, reset tonullinStart()(a freshsystem/initis always authoritative again). SetModel(model)resolvesmodel(the alias/value the caller passed, e.g."haiku") againstInitializeResponse.Response["models"]the same wayHeaderView.ParseModelsdoes (value->resolvedModel), falling back to the rawmodelstring when no match is found (manual/unlisted model ids still get a best-effort label instead of silently keeping the old one). It then tracks the outbound request with a callback (thePendingRequestMap.Trackcallback parameter already existed and was unused here) that commits_liveModelONLY when the resultingcontrol_responseisSuccess == true. A failed or timed-outset_modelleaves_liveModeluntouched, matching the CLI’s own behavior of leaving the previous model running.- A new public
CurrentModelproperty:_liveModel ?? InitMessage?.Model. This is additive —InitMessage.Modelitself is untouched, so nothing that legitimately wants “the resolved model system/init reported” (none of the current call sites do) is affected.
StatusBarView.ResolveModelName, StatusBarView.RefreshContextMeter’s
currentResolvedModel, and HeaderView.OnModelPickerClicked’s isCurrent
comparison now all read client.CurrentModel instead of
client.InitMessage.Model. This supersedes the “現在選択中モデルのハイライトは
client.InitMessage.Model…と比較する” line in the 2026-07-31
history-restore-flow-and-model-picker.md note section 3 — read CurrentModel
wherever that note says InitMessage.Model for “the current model” from now
on.
AgentHub.SwitchModel additionally resets _lastModelUsage to a fresh empty
dictionary (the same pattern StartFresh/SwitchToSession already use),
so the ctx meter shows “no data” (hidden) rather than a stale model’s
percentage under the new model’s label until the next turn’s modelUsage
arrives.
Tradeoffs accepted
Section titled “Tradeoffs accepted”- If the CLI ever silently ignores a
modelvalue that IS in themodels[]list (accepts the control_request withsuccess:truebut does not actually switch), the panel would show the new label anyway. No evidence of this happening has been captured; the alternative (never trustingsuccess:true) would make the picker permanently stuck showing the old model, which is strictly worse for the common case. CurrentModelresets toInitMessage.Modelonly on the nextStart()(full reconnect), not on e.g.Stop()alone — consistent with every other per-connection field this class already resets exactly once, inStart().
Regression-guard tests
Section titled “Regression-guard tests”All in AgentClientStateTests.cs (Core-layer coverage; StatusBarView.ResolveModelName
and HeaderView’s picker are thin one-line read-throughs of AgentClient.CurrentModel
with no InternalsVisibleTo from Tests/Editor into the UI assembly, so the behavior both
actually delegate to is exercised at the AgentClient level instead):
CurrentModel_MirrorsInitMessage_WhenNoLiveSwitchHasHappenedSetModel_CommitsLiveModelOnSuccess_AndResolvesAliasViaModelsList— asserts the resolved name ("claude-haiku-4-5-20251001"), not the raw alias ("haiku"), only commits once the matchingcontrol_responsearrives, and thatInitMessage.Modelitself stays untouched.SetModel_DoesNotCommitLiveModelOnFailureOrTimeoutSetModel_UnmatchedValue_FallsBackToRawValueOnSuccessStart_ResetsLiveModel_SoAFreshConnectIsAuthoritativeAgain
AgentHub.SwitchModel’s _lastModelUsage reset was left without a dedicated test
(the field is private/static and only observable through AgentHub.LastModelUsage;
the pre-existing StartFresh/SwitchToSession clears of the same field were also
never given a dedicated regression test) — consistent with this codebase’s existing
test-coverage boundary for AgentHub’s static composition-root methods.