From 5f81147c4d0c930c2ee415d46677eb6ad87e7e43 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 01:07:58 +0100 Subject: [PATCH] fix: persist embedded runtime context budget --- CHANGELOG.md | 1 + src/agents/command/session-store.test.ts | 41 ++++++++++++++++++++ src/agents/command/session-store.ts | 28 ++++++++----- src/agents/pi-embedded-runner/run.ts | 5 +++ src/agents/pi-embedded-runner/run/helpers.ts | 2 + src/agents/pi-embedded-runner/types.ts | 1 + src/auto-reply/reply/agent-runner.ts | 10 ++++- 7 files changed, 78 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aac33f51177..86f4eb093ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai - Slack/files: return non-image `download-file` results as local file paths instead of image payloads, and include Slack file IDs in inbound file placeholders so agents can call `download-file`. Fixes #71212. Thanks @teamrazo. - Browser control: scope standalone loopback auth to the resolved active gateway credential and fail closed when password mode lacks a resolved password, so inactive tokens or passwords no longer authorize browser routes. Fixes #65626. (#65639) Thanks @coygeek. - Control UI/Codex harness: emit native Codex app-server assistant and lifecycle completion events so live webchat runs stop spinning without needing a transcript reload fallback. (#70815) Thanks @lesaai. +- Agents/sessions: persist the runtime-resolved context budget from embedded agent runs, so Codex GPT-5.5 sessions keep the catalog/runtime context cap instead of falling back to the generic 200k status value. Fixes #71294. Thanks @tud0r. - Discord/replies: run `message_sending` plugin hooks for Discord reply delivery, including DM targets, so plugins can transform or cancel outbound Discord replies consistently with other channels. Fixes #59350. (#71094) Thanks @wei840222. - Control UI/commands: carry provider-owned thinking option ids/labels in session rows and defaults so fresh sessions show and accept dynamic modes such as `adaptive`, `xhigh`, and `max`. Fixes #71269. Thanks @Young-Khalil. - Image generation: make explicit `model=` overrides exact-only so failed `openai/gpt-image-2` requests no longer fall through to Gemini or other configured providers, and update `image_generate list` to mention OpenAI Codex OAuth as valid auth for `openai/gpt-image-2`. Fixes #71290 and #71231. Thanks @Young-Khalil and @steipete. diff --git a/src/agents/command/session-store.test.ts b/src/agents/command/session-store.test.ts index a4f0ebf0d14..5b39d1f7fdd 100644 --- a/src/agents/command/session-store.test.ts +++ b/src/agents/command/session-store.test.ts @@ -185,6 +185,47 @@ describe("updateSessionStoreAfterAgentRun", () => { }); }); + it("uses the runtime context budget from agent metadata instead of cold fallback", async () => { + await withTempSessionStore(async ({ storePath }) => { + const cfg = {} as OpenClawConfig; + const sessionKey = "agent:main:explicit:test-runtime-context"; + const sessionId = "test-runtime-context-session"; + const sessionStore: Record = { + [sessionKey]: { + sessionId, + updatedAt: 1, + }, + }; + await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); + + const result: EmbeddedPiRunResult = { + meta: { + durationMs: 1, + agentMeta: { + sessionId, + provider: "openai-codex", + model: "gpt-5.5", + contextTokens: 400_000, + }, + }, + }; + + await updateSessionStoreAfterAgentRun({ + cfg, + sessionId, + sessionKey, + storePath, + sessionStore, + defaultProvider: "openai-codex", + defaultModel: "gpt-5.5", + result, + }); + + expect(sessionStore[sessionKey]?.contextTokens).toBe(400_000); + expect(loadSessionStore(storePath)[sessionKey]?.contextTokens).toBe(400_000); + }); + }); + it("clears the embedded harness pin after a CLI run", async () => { await withTempSessionStore(async ({ storePath }) => { const cfg = { diff --git a/src/agents/command/session-store.ts b/src/agents/command/session-store.ts index 6e332327840..397c287d342 100644 --- a/src/agents/command/session-store.ts +++ b/src/agents/command/session-store.ts @@ -30,6 +30,13 @@ function resolveNonNegativeNumber(value: number | undefined): number | undefined return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; } +function resolvePositiveInteger(value: number | undefined): number | undefined { + if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { + return undefined; + } + return Math.floor(value); +} + export async function updateSessionStoreAfterAgentRun(params: { cfg: OpenClawConfig; contextTokensOverride?: number; @@ -62,16 +69,19 @@ export async function updateSessionStoreAfterAgentRun(params: { const modelUsed = result.meta.agentMeta?.model ?? fallbackModel ?? defaultModel; const providerUsed = result.meta.agentMeta?.provider ?? fallbackProvider ?? defaultProvider; const agentHarnessId = normalizeOptionalString(result.meta.agentMeta?.agentHarnessId); + const runtimeContextTokens = resolvePositiveInteger(result.meta.agentMeta?.contextTokens); const contextTokens = - typeof params.contextTokensOverride === "number" && params.contextTokensOverride > 0 - ? params.contextTokensOverride - : ((await getContextModule()).resolveContextTokensForModel({ - cfg, - provider: providerUsed, - model: modelUsed, - fallbackContextTokens: DEFAULT_CONTEXT_TOKENS, - allowAsyncLoad: false, - }) ?? DEFAULT_CONTEXT_TOKENS); + runtimeContextTokens !== undefined + ? runtimeContextTokens + : typeof params.contextTokensOverride === "number" && params.contextTokensOverride > 0 + ? params.contextTokensOverride + : ((await getContextModule()).resolveContextTokensForModel({ + cfg, + provider: providerUsed, + model: modelUsed, + fallbackContextTokens: DEFAULT_CONTEXT_TOKENS, + allowAsyncLoad: false, + }) ?? DEFAULT_CONTEXT_TOKENS); const entry = sessionStore[sessionKey] ?? { sessionId, diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 497856dcf57..26a30eddf63 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -777,6 +777,7 @@ export async function runEmbeddedPiAgent( sessionId: params.sessionId, provider, model: model.id, + contextTokens: ctxInfo.tokens, usageAccumulator, lastRunPromptUsage, lastTurnTotal, @@ -1371,6 +1372,7 @@ export async function runEmbeddedPiAgent( sessionId: sessionIdUsed, provider, model: model.id, + contextTokens: ctxInfo.tokens, usageAccumulator, lastRunPromptUsage, lastAssistant: sessionLastAssistant, @@ -1426,6 +1428,7 @@ export async function runEmbeddedPiAgent( sessionId: sessionIdUsed, provider, model: model.id, + contextTokens: ctxInfo.tokens, usageAccumulator, lastRunPromptUsage, lastAssistant: sessionLastAssistant, @@ -1465,6 +1468,7 @@ export async function runEmbeddedPiAgent( sessionId: sessionIdUsed, provider, model: model.id, + contextTokens: ctxInfo.tokens, usageAccumulator, lastRunPromptUsage, lastAssistant: sessionLastAssistant, @@ -1776,6 +1780,7 @@ export async function runEmbeddedPiAgent( sessionId: sessionIdUsed, provider: sessionLastAssistant?.provider ?? provider, model: sessionLastAssistant?.model ?? model.id, + contextTokens: ctxInfo.tokens, agentHarnessId: attempt.agentHarnessId, usage: usageMeta.usage, lastCallUsage: usageMeta.lastCallUsage, diff --git a/src/agents/pi-embedded-runner/run/helpers.ts b/src/agents/pi-embedded-runner/run/helpers.ts index 022335f95a8..3316af28ff2 100644 --- a/src/agents/pi-embedded-runner/run/helpers.ts +++ b/src/agents/pi-embedded-runner/run/helpers.ts @@ -124,6 +124,7 @@ export function buildErrorAgentMeta(params: { sessionId: string; provider: string; model: string; + contextTokens?: number; usageAccumulator: UsageAccumulator; lastRunPromptUsage: UsageSnapshot | undefined; lastAssistant?: { usage?: unknown } | null; @@ -139,6 +140,7 @@ export function buildErrorAgentMeta(params: { sessionId: params.sessionId, provider: params.provider, model: params.model, + ...(params.contextTokens ? { contextTokens: params.contextTokens } : {}), ...(usageMeta.usage ? { usage: usageMeta.usage } : {}), ...(usageMeta.lastCallUsage ? { lastCallUsage: usageMeta.lastCallUsage } : {}), ...(usageMeta.promptTokens ? { promptTokens: usageMeta.promptTokens } : {}), diff --git a/src/agents/pi-embedded-runner/types.ts b/src/agents/pi-embedded-runner/types.ts index f9c1badaa36..ed1e41e4d4f 100644 --- a/src/agents/pi-embedded-runner/types.ts +++ b/src/agents/pi-embedded-runner/types.ts @@ -6,6 +6,7 @@ export type EmbeddedPiAgentMeta = { sessionId: string; provider: string; model: string; + contextTokens?: number; agentHarnessId?: string; cliSessionBinding?: CliSessionBinding; compactionCount?: number; diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index bff832293bd..93e249c91f5 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -1324,7 +1324,14 @@ export async function runReplyAgent(params: { const cliSessionBinding = isCliProvider(providerUsed, cfg) ? runResult.meta?.agentMeta?.cliSessionBinding : undefined; + const runtimeContextTokens = + typeof runResult.meta?.agentMeta?.contextTokens === "number" && + Number.isFinite(runResult.meta.agentMeta.contextTokens) && + runResult.meta.agentMeta.contextTokens > 0 + ? Math.floor(runResult.meta.agentMeta.contextTokens) + : undefined; const contextTokensUsed = + runtimeContextTokens ?? resolveContextTokensForModel({ cfg, provider: providerUsed, @@ -1332,7 +1339,8 @@ export async function runReplyAgent(params: { contextTokensOverride: agentCfgContextTokens, fallbackContextTokens: activeSessionEntry?.contextTokens ?? DEFAULT_CONTEXT_TOKENS, allowAsyncLoad: false, - }) ?? DEFAULT_CONTEXT_TOKENS; + }) ?? + DEFAULT_CONTEXT_TOKENS; await persistRunSessionUsage({ storePath,