fix: persist embedded runtime context budget

This commit is contained in:
Peter Steinberger
2026-04-25 01:07:58 +01:00
parent 59f8a2c3fa
commit 5f81147c4d
7 changed files with 78 additions and 10 deletions

View File

@@ -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.

View File

@@ -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<string, SessionEntry> = {
[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 = {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 } : {}),

View File

@@ -6,6 +6,7 @@ export type EmbeddedPiAgentMeta = {
sessionId: string;
provider: string;
model: string;
contextTokens?: number;
agentHarnessId?: string;
cliSessionBinding?: CliSessionBinding;
compactionCount?: number;

View File

@@ -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,