Files
hapi/web/src
Junmo KimandGitHub e45fde51e9 fix(claude): stop 1M/200k context-window flicker in the status bar (#992)
* refactor(claude): thread session's selected model into SDKToLogConverter

Adds an optional selectedModel field to the converter's context, wired
from session.getModel() in the launcher, so a later commit can seed the
turn-1 contextWindow estimate for presets whose system/init model
arrives without the "[1m]" suffix. No behavior change yet.

* fix(claude): key contextWindow cache by model to stop 1M/200k flicker

The remote launcher re-emits system/init on every turn for the same
converter instance. Its init-time estimate only checked whether the
model string ended in "[1m]", but current claude CLI versions strip
that suffix from system/init for some 1M presets (fable[1m] arrives as
"claude-fable-5"), so the estimate guessed 200k for them. The one
authoritative value is result.modelUsage[<model>].contextWindow, which
arrives after the heuristic has already injected 200k into that turn's
assistant message and then gets clobbered back to 200k by the very
next turn's init - producing the observed 200k<->1M oscillation in the
web status bar.

Cache the authoritative contextWindow per model id instead of a single
session-wide number, and only let system/init seed a heuristic guess
for a model that has no cached value yet, so a same-model re-init no
longer downgrades an already-learned value.

Two observed facts about the CLI's model ids drive the design:
system/init.model and the result.modelUsage keys always agree with
each other within a session (both bare for plain/fable[1m], both
suffixed for opus[1m]/sonnet[1m]), while each per-turn assistant
message reports its model bare and thus can't distinguish a 200k plain
preset from its 1M "[1m]" variant on tiers where they share a base id.
So the cache is keyed on the raw id (init/result agree, no
normalization) and assistant injection looks the value up via
resolvedModel (the last init id) rather than the lossy message.model.
Keying raw keeps a plain preset and its [1m] variant on distinct
entries; looking up via resolvedModel also means sidechain (Task
subagent) messages carry the main session window rather than the
subagent's own, since the web status bar picks the most recent usage
message without filtering sidechains and would otherwise flicker to
the subagent's smaller window while it runs.

For presets whose init model arrives bare even though they are 1M
(fable[1m]), the originally-selected preset - which preserves the
"[1m]" suffix - seeds the turn-1 estimate, kept live across mid-session
model switches via updateSelectedModel() (called from the launcher on
every turn) so it never goes stale.

* fix(web): recognize [1m] suffix on full Claude model ids in budget fallback

getContextBudgetTokens already special-cased "[1m]" for short preset
values (e.g. "opus[1m]") but fell through to the default 200k budget
for full model ids (e.g. "claude-opus-4-8[1m]"), which is what the CLI
now reports once context_window isn't available and this fallback is
consulted. Check the suffix on that branch too so it stays a correct
last-resort even without a session-provided context_window.

* refactor(web): merge duplicate Claude context-budget branches

isClaudeModelPreset(trimmedModel) and the startsWith('claude-') branch
below it had become byte-for-byte identical bodies after the [1m]
suffix check was added to both. Merge them into one condition; no
behavior change.

* fix(claude): distinguish fable from fable[1m] when the CLI reports both bare

The per-model contextWindow cache keyed on the raw system/init model id,
on the assumption that a 1M preset and its plain form always land on
distinct ids. That holds for opus[1m]/sonnet[1m] (the CLI reports the
"[1m]" suffix on their init and result ids) but not for fable: the CLI
reports both "fable" and "fable[1m]" with the same bare id
"claude-fable-5". So the "seed only if not already cached" guard would
skip re-seeding when switching fable[1m] -> fable, leaving the stale 1M
in place until fable's own result arrived - the same switch flicker this
change set out to remove, just for fable specifically.

Fold the selected preset's "[1m]" back into the cache key
(computeContextWindowKey): when the init model arrives bare but the
session selected an "[1m]" preset, key the entry as "<id>[1m]" so the 1M
and plain variants stay distinct; ids the CLI already suffixed are left
as-is. Seeding, lookups, and the current-model result entry all use this
resolved key. Subagent result entries (e.g. haiku) keep their own raw id
so the session's "[1m]" is never folded onto a model that isn't the
selected one.
2026-07-11 10:39:36 +08:00
..
2026-05-21 10:44:46 +08:00