mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-18 12:14:32 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user