Skip to content

Commit f046d7a

Browse files
authored
fix(status): ignore stale context after model switch (#93306)
Summary: - The PR changes `/status` context-window selection to ignore stale runtime snapshots after manual model switches while preserving fallback/runtime-alias context windows. - PR surface: Source +6, Tests +128. Total +134 across 2 files. - Reproducibility: yes. source-reproducible: current main trusts explicit runtime context before checking fall ... fer. I did not run a local failing repro, but the PR fixture models the stale prior-runtime state directly. Automerge notes: - PR branch already contained follow-up commit before automerge: test(status): make context fixtures type-correct Validation: - ClawSweeper review passed for head f14fda4. - Required merge gates passed before the squash merge. Prepared head SHA: f14fda4 Review: #93306 (comment) Co-authored-by: Mason Huang <[email protected]> Approved-by: hxy91819
1 parent de1d329 commit f046d7a

2 files changed

Lines changed: 148 additions & 14 deletions

File tree

src/status/status-message.test.ts

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
// Status message tests cover status message formatting and persistence.
2-
import { describe, expect, it } from "vitest";
2+
import { afterEach, describe, expect, it } from "vitest";
3+
import { testing as cliBackendsTesting } from "../agents/cli-backends.js";
4+
import type { ModelDefinitionConfig } from "../config/types.models.js";
35
import { formatFastModeLabel } from "./status-labels.js";
6+
import { buildStatusMessage } from "./status-message.js";
7+
8+
function statusTestModel(id: string, name: string, contextWindow: number): ModelDefinitionConfig {
9+
return {
10+
id,
11+
name,
12+
reasoning: false,
13+
input: ["text"],
14+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
15+
contextWindow,
16+
maxTokens: 8_192,
17+
};
18+
}
19+
20+
afterEach(() => {
21+
cliBackendsTesting.resetDepsForTest();
22+
});
423

524
describe("formatFastModeLabel", () => {
625
it("shows fast mode when enabled", () => {
@@ -11,3 +30,112 @@ describe("formatFastModeLabel", () => {
1130
expect(formatFastModeLabel(false)).toBe("Fast: off");
1231
});
1332
});
33+
34+
describe("buildStatusMessage context window", () => {
35+
it("ignores stale runtime context after a manual session model switch", () => {
36+
const text = buildStatusMessage({
37+
config: {
38+
models: {
39+
providers: {
40+
"ollama-cloud": {
41+
baseUrl: "https://ollama.com",
42+
models: [
43+
statusTestModel("deepseek-v4-pro", "DeepSeek V4 Pro", 1_000_000),
44+
statusTestModel("glm-5.1", "GLM 5.1", 200_000),
45+
],
46+
},
47+
},
48+
},
49+
},
50+
agent: {
51+
model: "ollama-cloud/deepseek-v4-pro",
52+
contextTokens: 1_000_000,
53+
},
54+
configuredDefaultModelLabel: "ollama-cloud/deepseek-v4-pro",
55+
explicitConfiguredContextTokens: 1_000_000,
56+
runtimeContextTokens: 1_000_000,
57+
sessionEntry: {
58+
sessionId: "manual-switch-stale-runtime",
59+
updatedAt: 0,
60+
providerOverride: "ollama-cloud",
61+
modelOverride: "glm-5.1",
62+
modelOverrideSource: "user",
63+
modelProvider: "ollama-cloud",
64+
model: "deepseek-v4-pro",
65+
totalTokens: 128_393,
66+
totalTokensFresh: true,
67+
},
68+
sessionKey: "agent:main:telegram:direct:584667058",
69+
sessionScope: "per-sender",
70+
queue: { mode: "steer", depth: 0 },
71+
modelAuth: "api-key",
72+
});
73+
74+
expect(text).toContain("Session selected: ollama-cloud/glm-5.1");
75+
expect(text).toContain("Context: 128k/200k");
76+
expect(text).not.toContain("Context: 128k/1.0m");
77+
});
78+
79+
it("keeps trusted runtime context for config-backed runtime aliases", () => {
80+
cliBackendsTesting.setDepsForTest({
81+
resolvePluginSetupCliBackend: ({ backend }) =>
82+
backend === "claude-cli"
83+
? {
84+
pluginId: "anthropic",
85+
backend: {
86+
id: "claude-cli",
87+
modelProvider: "anthropic",
88+
config: { command: "claude" },
89+
bundleMcp: false,
90+
},
91+
}
92+
: undefined,
93+
resolvePluginSetupRegistry: () => {
94+
throw new Error("setup registry should not load for a targeted runtime alias");
95+
},
96+
resolveRuntimeCliBackends: () => [],
97+
});
98+
99+
const text = buildStatusMessage({
100+
config: {
101+
agents: {
102+
defaults: {
103+
cliBackends: {
104+
"claude-cli": { command: "claude" },
105+
},
106+
},
107+
},
108+
models: {
109+
providers: {
110+
anthropic: {
111+
baseUrl: "https://api.anthropic.com",
112+
models: [statusTestModel("claude-haiku-4-5", "Claude Haiku 4.5", 200_000)],
113+
},
114+
},
115+
},
116+
},
117+
agent: {
118+
model: "anthropic/claude-haiku-4-5",
119+
contextTokens: 200_000,
120+
},
121+
runtimeContextTokens: 1_000_000,
122+
sessionEntry: {
123+
sessionId: "runtime-alias-context",
124+
updatedAt: 0,
125+
modelProvider: "claude-cli",
126+
model: "claude-haiku-4-5",
127+
totalTokens: 36_000,
128+
totalTokensFresh: true,
129+
},
130+
sessionKey: "agent:main:main",
131+
sessionScope: "per-sender",
132+
queue: { mode: "collect", depth: 0 },
133+
modelAuth: "oauth",
134+
activeModelAuth: "oauth",
135+
});
136+
137+
expect(text).toContain("Model: anthropic/claude-haiku-4-5");
138+
expect(text).toContain("Context: 36k/1.0m");
139+
expect(text).not.toContain("Context: 36k/200k");
140+
});
141+
});

src/status/status-message.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -788,21 +788,24 @@ export function buildStatusMessage(args: StatusArgs): string {
788788
? (cappedAgentContextTokens ?? activeContextTokens)
789789
: cappedAgentContextTokens))
790790
: undefined;
791+
const runtimeSnapshotHasFallbackProvenance =
792+
initialFallbackState.active ||
793+
hasSessionAutoModelFallbackProvenance(entry) ||
794+
areRuntimeModelRefsEquivalent(activeModelLabel, modelRefs.selected.label || "unknown", {
795+
config: args.config,
796+
});
791797
// When a fallback model is active, the selected-model context limit that
792798
// callers keep on the agent config is often stale. Prefer an explicit runtime
793-
// snapshot when available. Separately, callers can pass an explicit configured
794-
// cap that should still apply on fallback paths, but it cannot exceed the
795-
// active runtime window when that window is known. Persisted runtime snapshots
796-
// still take precedence over configured caps so historical fallback sessions
797-
// keep their last known live limit even if the active model later becomes
798-
// unresolvable.
799+
// snapshot only when it belongs to a real fallback/equivalent runtime. A
800+
// transcript-derived previous model is stale after a manual switch and must
801+
// not pin the newly selected model to the old context window. Separately,
802+
// callers can pass an explicit configured cap that should still apply on
803+
// fallback paths, but it cannot exceed the active runtime window when that
804+
// window is known. Persisted runtime snapshots still take precedence over
805+
// configured caps so historical fallback sessions keep their last known live
806+
// limit even if the active model later becomes unresolvable.
799807
const contextTokens = runtimeDiffersFromSelected
800-
? (explicitRuntimeContextTokens ??
801-
(() => {
802-
const runtimeSnapshotHasFallbackProvenance =
803-
initialFallbackState.active ||
804-
hasSessionAutoModelFallbackProvenance(entry) ||
805-
areRuntimeModelRefsEquivalent(activeModelLabel, modelRefs.selected.label || "unknown");
808+
? (() => {
806809
if (!runtimeSnapshotHasFallbackProvenance) {
807810
if (typeof selectedContextTokens === "number") {
808811
if (explicitConfiguredContextTokens !== undefined) {
@@ -821,6 +824,9 @@ export function buildStatusMessage(args: StatusArgs): string {
821824
}
822825
return DEFAULT_CONTEXT_TOKENS;
823826
}
827+
if (explicitRuntimeContextTokens !== undefined) {
828+
return explicitRuntimeContextTokens;
829+
}
824830
if (cappedPersistedContextTokens !== undefined) {
825831
const trustedPersistedContextTokens = cappedPersistedContextTokens;
826832
const persistedLooksSelectedWindow =
@@ -852,7 +858,7 @@ export function buildStatusMessage(args: StatusArgs): string {
852858
return activeContextTokens;
853859
}
854860
return DEFAULT_CONTEXT_TOKENS;
855-
})())
861+
})()
856862
: (resolveContextTokensForModel({
857863
cfg: contextConfig,
858864
...(contextLookupProvider ? { provider: contextLookupProvider } : {}),

0 commit comments

Comments
 (0)