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>
This commit is contained in:
Neerav Makwana
2026-04-09 07:55:34 -04:00
committed by GitHub
parent 1ee4a1606e
commit 2645ed154b
8 changed files with 146 additions and 18 deletions

View File

@@ -191,6 +191,7 @@ Docs: https://docs.openclaw.ai
- Providers/Z.AI: default onboarding and endpoint detection to GLM-5.1 instead of GLM-5. (#61998) Thanks @serg0x.
- 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.
- 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.
- 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.
## 2026.4.5

View File

@@ -358,6 +358,8 @@ export async function runPreflightCompactionIfNeeded(params: {
}
const contextWindowTokens = resolveMemoryFlushContextWindowTokens({
cfg: params.cfg,
provider: params.followupRun.run.provider,
modelId: params.followupRun.run.model ?? params.defaultModel,
agentCfgContextTokens: params.agentCfgContextTokens,
});
@@ -523,6 +525,8 @@ export async function runMemoryFlushIfNeeded(params: {
params.sessionEntry ??
(params.sessionKey ? params.sessionStore?.[params.sessionKey] : undefined);
const contextWindowTokens = resolveMemoryFlushContextWindowTokens({
cfg: params.cfg,
provider: params.followupRun.run.provider,
modelId: params.followupRun.run.model ?? params.defaultModel,
agentCfgContextTokens: params.agentCfgContextTokens,
});

View File

@@ -1,4 +1,4 @@
import { lookupContextTokens } from "../../agents/context.js";
import { resolveContextTokensForModel } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import { resolveModelAuthMode } from "../../agents/model-auth.js";
import { isCliProvider } from "../../agents/model-selection.js";
@@ -501,10 +501,14 @@ export async function runReplyAgent(params: {
? runResult.meta?.agentMeta?.cliSessionBinding
: undefined;
const contextTokensUsed =
agentCfgContextTokens ??
lookupContextTokens(modelUsed) ??
activeSessionEntry?.contextTokens ??
DEFAULT_CONTEXT_TOKENS;
resolveContextTokensForModel({
cfg,
provider: providerUsed,
model: modelUsed,
contextTokensOverride: agentCfgContextTokens,
fallbackContextTokens: activeSessionEntry?.contextTokens ?? DEFAULT_CONTEXT_TOKENS,
allowAsyncLoad: false,
}) ?? DEFAULT_CONTEXT_TOKENS;
await persistRunSessionUsage({
storePath,

View File

@@ -3,7 +3,7 @@ import {
resolveDefaultAgentId,
resolveSessionAgentId,
} from "../../agents/agent-scope.js";
import { lookupCachedContextTokens } from "../../agents/context-cache.js";
import { resolveContextTokensForModel } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import type { ModelAliasIndex } from "../../agents/model-selection.js";
import type { OpenClawConfig } from "../../config/config.js";
@@ -221,6 +221,12 @@ export async function persistInlineDirectives(params: {
provider,
model,
contextTokens:
agentCfg?.contextTokens ?? lookupCachedContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS,
resolveContextTokensForModel({
cfg,
provider,
model,
contextTokensOverride: agentCfg?.contextTokens,
allowAsyncLoad: false,
}) ?? DEFAULT_CONTEXT_TOKENS,
};
}

View File

@@ -1103,6 +1103,63 @@ describe("createFollowupRunner messaging tool dedupe", () => {
persistSpy.mockRestore();
});
it("uses providerUsed for snapshot freshness when agent metadata overrides the run provider", async () => {
const storePath = "/tmp/openclaw-followup-usage-provider.json";
const sessionKey = "main";
const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() };
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
const persistSpy = vi.spyOn(sessionRunAccounting, "persistRunSessionUsage");
runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [{ text: "hello world!" }],
meta: {
agentMeta: {
usage: { input: 10, output: 5 },
lastCallUsage: { input: 6, output: 3 },
model: "claude-opus-4-6",
provider: "anthropic",
},
},
});
const runner = createFollowupRunner({
opts: { onBlockReply: createAsyncReplySpy() },
typing: createMockTypingController(),
typingMode: "instant",
defaultModel: "anthropic/claude-opus-4-6",
sessionEntry,
sessionStore,
sessionKey,
storePath,
});
await expect(
runner(
createQueuedRun({
run: {
provider: "openai",
config: {
agents: {
defaults: {
cliBackends: {
anthropic: {},
},
},
},
} as OpenClawConfig,
},
}),
),
).resolves.toBeUndefined();
expect(persistSpy).toHaveBeenCalledWith(
expect.objectContaining({
providerUsed: "anthropic",
usageIsContextSnapshot: true,
}),
);
persistSpy.mockRestore();
});
it("does not fall back to dispatcher when cross-channel origin routing fails", async () => {
routeReplyMock.mockResolvedValueOnce({
ok: false,

View File

@@ -5,7 +5,7 @@ import {
} from "openclaw/plugin-sdk/reply-payload";
import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import { lookupContextTokens } from "../../agents/context.js";
import { resolveContextTokensForModel } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { isCliProvider } from "../../agents/model-selection.js";
@@ -289,11 +289,17 @@ export function createFollowupRunner(params: {
const usage = runResult.meta?.agentMeta?.usage;
const promptTokens = runResult.meta?.agentMeta?.promptTokens;
const modelUsed = runResult.meta?.agentMeta?.model ?? fallbackModel ?? defaultModel;
const providerUsed =
runResult.meta?.agentMeta?.provider ?? fallbackProvider ?? queued.run.provider;
const contextTokensUsed =
agentCfgContextTokens ??
lookupContextTokens(modelUsed) ??
sessionEntry?.contextTokens ??
DEFAULT_CONTEXT_TOKENS;
resolveContextTokensForModel({
cfg: queued.run.config,
provider: providerUsed,
model: modelUsed,
contextTokensOverride: agentCfgContextTokens,
fallbackContextTokens: sessionEntry?.contextTokens ?? DEFAULT_CONTEXT_TOKENS,
allowAsyncLoad: false,
}) ?? DEFAULT_CONTEXT_TOKENS;
if (storePath && sessionKey) {
await persistRunSessionUsage({
@@ -304,11 +310,11 @@ export function createFollowupRunner(params: {
lastCallUsage: runResult.meta?.agentMeta?.lastCallUsage,
promptTokens,
modelUsed,
providerUsed: fallbackProvider,
providerUsed,
contextTokensUsed,
systemPromptReport: runResult.meta?.systemPromptReport,
cliSessionBinding: runResult.meta?.agentMeta?.cliSessionBinding,
usageIsContextSnapshot: isCliProvider(fallbackProvider ?? run.provider, runtimeConfig),
usageIsContextSnapshot: isCliProvider(providerUsed, runtimeConfig),
logLabel: "followup",
});
}

View File

@@ -1,16 +1,23 @@
import crypto from "node:crypto";
import { lookupContextTokens } from "../../agents/context.js";
import { resolveContextTokensForModel } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveFreshSessionTotalTokens, type SessionEntry } from "../../config/sessions.js";
export function resolveMemoryFlushContextWindowTokens(params: {
modelId?: string;
agentCfgContextTokens?: number;
cfg?: OpenClawConfig;
provider?: string;
}): number {
return (
lookupContextTokens(params.modelId, { allowAsyncLoad: false }) ??
params.agentCfgContextTokens ??
DEFAULT_CONTEXT_TOKENS
resolveContextTokensForModel({
cfg: params.cfg,
provider: params.provider,
model: params.modelId,
contextTokensOverride: params.agentCfgContextTokens,
allowAsyncLoad: false,
}) ?? DEFAULT_CONTEXT_TOKENS
);
}

View File

@@ -369,6 +369,49 @@ describe("resolveMemoryFlushContextWindowTokens", () => {
it("falls back to agent config or default tokens", () => {
expect(resolveMemoryFlushContextWindowTokens({ agentCfgContextTokens: 42_000 })).toBe(42_000);
});
it("uses provider-specific configured limits when the same model id exists on multiple providers", () => {
const cfg = {
models: {
providers: {
"provider-a": { models: [{ id: "shared-model", contextWindow: 200_000 }] },
"provider-b": { models: [{ id: "shared-model", contextWindow: 512_000 }] },
},
},
};
expect(
resolveMemoryFlushContextWindowTokens({
cfg: cfg as never,
provider: "provider-b",
modelId: "shared-model",
}),
).toBe(512_000);
expect(
resolveMemoryFlushContextWindowTokens({
cfg: cfg as never,
provider: "provider-a",
modelId: "shared-model",
}),
).toBe(200_000);
});
it("prefers agent contextTokens override over the provider configured window", () => {
const cfg = {
models: {
providers: {
"provider-b": { models: [{ id: "shared-model", contextWindow: 512_000 }] },
},
},
};
expect(
resolveMemoryFlushContextWindowTokens({
cfg: cfg as never,
provider: "provider-b",
modelId: "shared-model",
agentCfgContextTokens: 100_000,
}),
).toBe(100_000);
});
});
describe("incrementCompactionCount", () => {