Skip to content

Commit 2645ed1

Browse files
fix: provider-qualified session context limits (#62493) (thanks @neeravmakwana)
* fix(sessions): provider-qualified context limits (#62472) * fix(sessions): honor agent context cap in memory-flush gate * refactor(sessions): unify context token resolution * fix: keep followup snapshot freshness on the active provider (#62493) (thanks @neeravmakwana) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
1 parent 1ee4a16 commit 2645ed1

8 files changed

Lines changed: 146 additions & 18 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ Docs: https://docs.openclaw.ai
191191
- Providers/Z.AI: default onboarding and endpoint detection to GLM-5.1 instead of GLM-5. (#61998) Thanks @serg0x.
192192
- Cron/isolated: resolve auth profiles without treating every isolated run as a brand-new auth session, so profile-based providers (for example OpenRouter) keep a stable credential choice instead of rotating or ignoring stored keys. (#62783) Thanks @neeravmakwana.
193193
- CLI/tasks: `openclaw tasks cancel` now records operator cancellation for CLI runtime tasks instead of returning "Task runtime does not support cancellation yet", so stuck `running` CLI tasks can be cleared. (#62419) Thanks @neeravmakwana.
194+
- Sessions/context: resolve context window limits using the active provider plus model (not bare model id alone) when persisting session usage, applying inline directives, and sizing memory-flush / preflight compaction thresholds, so duplicate model ids across providers no longer leak the wrong `contextTokens` into the session store or `/status`. (#62472) Thanks @neeravmakwana.
194195

195196
## 2026.4.5
196197

src/auto-reply/reply/agent-runner-memory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,8 @@ export async function runPreflightCompactionIfNeeded(params: {
358358
}
359359

360360
const contextWindowTokens = resolveMemoryFlushContextWindowTokens({
361+
cfg: params.cfg,
362+
provider: params.followupRun.run.provider,
361363
modelId: params.followupRun.run.model ?? params.defaultModel,
362364
agentCfgContextTokens: params.agentCfgContextTokens,
363365
});
@@ -523,6 +525,8 @@ export async function runMemoryFlushIfNeeded(params: {
523525
params.sessionEntry ??
524526
(params.sessionKey ? params.sessionStore?.[params.sessionKey] : undefined);
525527
const contextWindowTokens = resolveMemoryFlushContextWindowTokens({
528+
cfg: params.cfg,
529+
provider: params.followupRun.run.provider,
526530
modelId: params.followupRun.run.model ?? params.defaultModel,
527531
agentCfgContextTokens: params.agentCfgContextTokens,
528532
});

src/auto-reply/reply/agent-runner.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { lookupContextTokens } from "../../agents/context.js";
1+
import { resolveContextTokensForModel } from "../../agents/context.js";
22
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
33
import { resolveModelAuthMode } from "../../agents/model-auth.js";
44
import { isCliProvider } from "../../agents/model-selection.js";
@@ -501,10 +501,14 @@ export async function runReplyAgent(params: {
501501
? runResult.meta?.agentMeta?.cliSessionBinding
502502
: undefined;
503503
const contextTokensUsed =
504-
agentCfgContextTokens ??
505-
lookupContextTokens(modelUsed) ??
506-
activeSessionEntry?.contextTokens ??
507-
DEFAULT_CONTEXT_TOKENS;
504+
resolveContextTokensForModel({
505+
cfg,
506+
provider: providerUsed,
507+
model: modelUsed,
508+
contextTokensOverride: agentCfgContextTokens,
509+
fallbackContextTokens: activeSessionEntry?.contextTokens ?? DEFAULT_CONTEXT_TOKENS,
510+
allowAsyncLoad: false,
511+
}) ?? DEFAULT_CONTEXT_TOKENS;
508512

509513
await persistRunSessionUsage({
510514
storePath,

src/auto-reply/reply/directive-handling.persist.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
resolveDefaultAgentId,
44
resolveSessionAgentId,
55
} from "../../agents/agent-scope.js";
6-
import { lookupCachedContextTokens } from "../../agents/context-cache.js";
6+
import { resolveContextTokensForModel } from "../../agents/context.js";
77
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
88
import type { ModelAliasIndex } from "../../agents/model-selection.js";
99
import type { OpenClawConfig } from "../../config/config.js";
@@ -221,6 +221,12 @@ export async function persistInlineDirectives(params: {
221221
provider,
222222
model,
223223
contextTokens:
224-
agentCfg?.contextTokens ?? lookupCachedContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS,
224+
resolveContextTokensForModel({
225+
cfg,
226+
provider,
227+
model,
228+
contextTokensOverride: agentCfg?.contextTokens,
229+
allowAsyncLoad: false,
230+
}) ?? DEFAULT_CONTEXT_TOKENS,
225231
};
226232
}

src/auto-reply/reply/followup-runner.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,63 @@ describe("createFollowupRunner messaging tool dedupe", () => {
11031103
persistSpy.mockRestore();
11041104
});
11051105

1106+
it("uses providerUsed for snapshot freshness when agent metadata overrides the run provider", async () => {
1107+
const storePath = "/tmp/openclaw-followup-usage-provider.json";
1108+
const sessionKey = "main";
1109+
const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() };
1110+
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
1111+
const persistSpy = vi.spyOn(sessionRunAccounting, "persistRunSessionUsage");
1112+
runEmbeddedPiAgentMock.mockResolvedValueOnce({
1113+
payloads: [{ text: "hello world!" }],
1114+
meta: {
1115+
agentMeta: {
1116+
usage: { input: 10, output: 5 },
1117+
lastCallUsage: { input: 6, output: 3 },
1118+
model: "claude-opus-4-6",
1119+
provider: "anthropic",
1120+
},
1121+
},
1122+
});
1123+
1124+
const runner = createFollowupRunner({
1125+
opts: { onBlockReply: createAsyncReplySpy() },
1126+
typing: createMockTypingController(),
1127+
typingMode: "instant",
1128+
defaultModel: "anthropic/claude-opus-4-6",
1129+
sessionEntry,
1130+
sessionStore,
1131+
sessionKey,
1132+
storePath,
1133+
});
1134+
1135+
await expect(
1136+
runner(
1137+
createQueuedRun({
1138+
run: {
1139+
provider: "openai",
1140+
config: {
1141+
agents: {
1142+
defaults: {
1143+
cliBackends: {
1144+
anthropic: {},
1145+
},
1146+
},
1147+
},
1148+
} as OpenClawConfig,
1149+
},
1150+
}),
1151+
),
1152+
).resolves.toBeUndefined();
1153+
1154+
expect(persistSpy).toHaveBeenCalledWith(
1155+
expect.objectContaining({
1156+
providerUsed: "anthropic",
1157+
usageIsContextSnapshot: true,
1158+
}),
1159+
);
1160+
persistSpy.mockRestore();
1161+
});
1162+
11061163
it("does not fall back to dispatcher when cross-channel origin routing fails", async () => {
11071164
routeReplyMock.mockResolvedValueOnce({
11081165
ok: false,

src/auto-reply/reply/followup-runner.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from "openclaw/plugin-sdk/reply-payload";
66
import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js";
77
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
8-
import { lookupContextTokens } from "../../agents/context.js";
8+
import { resolveContextTokensForModel } from "../../agents/context.js";
99
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
1010
import { runWithModelFallback } from "../../agents/model-fallback.js";
1111
import { isCliProvider } from "../../agents/model-selection.js";
@@ -289,11 +289,17 @@ export function createFollowupRunner(params: {
289289
const usage = runResult.meta?.agentMeta?.usage;
290290
const promptTokens = runResult.meta?.agentMeta?.promptTokens;
291291
const modelUsed = runResult.meta?.agentMeta?.model ?? fallbackModel ?? defaultModel;
292+
const providerUsed =
293+
runResult.meta?.agentMeta?.provider ?? fallbackProvider ?? queued.run.provider;
292294
const contextTokensUsed =
293-
agentCfgContextTokens ??
294-
lookupContextTokens(modelUsed) ??
295-
sessionEntry?.contextTokens ??
296-
DEFAULT_CONTEXT_TOKENS;
295+
resolveContextTokensForModel({
296+
cfg: queued.run.config,
297+
provider: providerUsed,
298+
model: modelUsed,
299+
contextTokensOverride: agentCfgContextTokens,
300+
fallbackContextTokens: sessionEntry?.contextTokens ?? DEFAULT_CONTEXT_TOKENS,
301+
allowAsyncLoad: false,
302+
}) ?? DEFAULT_CONTEXT_TOKENS;
297303

298304
if (storePath && sessionKey) {
299305
await persistRunSessionUsage({
@@ -304,11 +310,11 @@ export function createFollowupRunner(params: {
304310
lastCallUsage: runResult.meta?.agentMeta?.lastCallUsage,
305311
promptTokens,
306312
modelUsed,
307-
providerUsed: fallbackProvider,
313+
providerUsed,
308314
contextTokensUsed,
309315
systemPromptReport: runResult.meta?.systemPromptReport,
310316
cliSessionBinding: runResult.meta?.agentMeta?.cliSessionBinding,
311-
usageIsContextSnapshot: isCliProvider(fallbackProvider ?? run.provider, runtimeConfig),
317+
usageIsContextSnapshot: isCliProvider(providerUsed, runtimeConfig),
312318
logLabel: "followup",
313319
});
314320
}

src/auto-reply/reply/memory-flush.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import crypto from "node:crypto";
2-
import { lookupContextTokens } from "../../agents/context.js";
2+
import { resolveContextTokensForModel } from "../../agents/context.js";
33
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
4+
import type { OpenClawConfig } from "../../config/config.js";
45
import { resolveFreshSessionTotalTokens, type SessionEntry } from "../../config/sessions.js";
56

67
export function resolveMemoryFlushContextWindowTokens(params: {
78
modelId?: string;
89
agentCfgContextTokens?: number;
10+
cfg?: OpenClawConfig;
11+
provider?: string;
912
}): number {
1013
return (
11-
lookupContextTokens(params.modelId, { allowAsyncLoad: false }) ??
12-
params.agentCfgContextTokens ??
13-
DEFAULT_CONTEXT_TOKENS
14+
resolveContextTokensForModel({
15+
cfg: params.cfg,
16+
provider: params.provider,
17+
model: params.modelId,
18+
contextTokensOverride: params.agentCfgContextTokens,
19+
allowAsyncLoad: false,
20+
}) ?? DEFAULT_CONTEXT_TOKENS
1421
);
1522
}
1623

src/auto-reply/reply/reply-state.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,49 @@ describe("resolveMemoryFlushContextWindowTokens", () => {
369369
it("falls back to agent config or default tokens", () => {
370370
expect(resolveMemoryFlushContextWindowTokens({ agentCfgContextTokens: 42_000 })).toBe(42_000);
371371
});
372+
373+
it("uses provider-specific configured limits when the same model id exists on multiple providers", () => {
374+
const cfg = {
375+
models: {
376+
providers: {
377+
"provider-a": { models: [{ id: "shared-model", contextWindow: 200_000 }] },
378+
"provider-b": { models: [{ id: "shared-model", contextWindow: 512_000 }] },
379+
},
380+
},
381+
};
382+
expect(
383+
resolveMemoryFlushContextWindowTokens({
384+
cfg: cfg as never,
385+
provider: "provider-b",
386+
modelId: "shared-model",
387+
}),
388+
).toBe(512_000);
389+
expect(
390+
resolveMemoryFlushContextWindowTokens({
391+
cfg: cfg as never,
392+
provider: "provider-a",
393+
modelId: "shared-model",
394+
}),
395+
).toBe(200_000);
396+
});
397+
398+
it("prefers agent contextTokens override over the provider configured window", () => {
399+
const cfg = {
400+
models: {
401+
providers: {
402+
"provider-b": { models: [{ id: "shared-model", contextWindow: 512_000 }] },
403+
},
404+
},
405+
};
406+
expect(
407+
resolveMemoryFlushContextWindowTokens({
408+
cfg: cfg as never,
409+
provider: "provider-b",
410+
modelId: "shared-model",
411+
agentCfgContextTokens: 100_000,
412+
}),
413+
).toBe(100_000);
414+
});
372415
});
373416

374417
describe("incrementCompactionCount", () => {

0 commit comments

Comments
 (0)