Skip to content

Commit 4004c93

Browse files
authored
fix: use Codex context windows for OpenAI runtime (#81906)
* fix: use Codex context windows for OpenAI runtime * test: satisfy status model fixture types * fix: note Codex context window budget fix
1 parent 9cfb4c7 commit 4004c93

10 files changed

Lines changed: 278 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Docs: https://docs.openclaw.ai
167167
- Agents/memory-flush: surface non-abort memory-flush failures (provider timeout, transport error, generic agent failure) as visible reply payloads so the outer reply loop short-circuits and isolated cron runs propagate the error into `meta.error` instead of completing silently with `status: "ok"` and an empty payload. Previously only the specific "Memory flush writes are restricted to ..." message was surfaced. Fixes #80755. Thanks @nailujac.
168168
- Channels/loop-guard: enforce shared per-pair bot loop protection in the core channel-turn kernel, with Discord, Slack, Matrix, and Google Chat supplying bot-pair facts where they can reliably identify accepted bot-authored messages. The generic guard keys on `(scope, conversation, participant pair)`, suppresses every additional bot-to-bot event in either direction once a pair crosses the configured budget, and lifts suppression after `cooldownSeconds`. Defaults are `maxEventsPerWindow: 20`, `windowSeconds: 60`, and `cooldownSeconds: 60` whenever a channel lets bot-authored messages reach dispatch; they can be set globally via `channels.defaults.botLoopProtection` and overridden per channel/account or supported per-conversation config. Fixes #58789. Thanks @pandadev66.
169169
- Agents/memory-flush: surface non-abort memory-flush failures (provider timeout, transport error, generic agent failure) as visible reply payloads so the outer reply loop short-circuits and isolated cron runs propagate the error into `meta.error` instead of completing silently with `status: "ok"` and an empty payload. Previously only the specific "Memory flush writes are restricted to ..." message was surfaced. Refs #80755. Thanks @kinjitakabe and @nailujac.
170+
- Codex harness: use the active Codex runtime context window for OpenAI-selected budgeting, manual `/compact`, and `/status`, so stale OpenAI session metadata no longer overstates context limits. (#81906) thanks @jalehman.
170171

171172
## 2026.5.12
172173

src/agents/openai-codex-routing.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,15 @@ export function resolveOpenAIRuntimeProviderForPi(params: {
149149
? OPENAI_CODEX_PROVIDER_ID
150150
: params.provider;
151151
}
152+
153+
export function resolveContextConfigProviderForRuntime(params: {
154+
provider: string;
155+
runtimeId?: string;
156+
}): string {
157+
const provider = normalizeProviderId(params.provider);
158+
const runtimeId = normalizeEmbeddedAgentRuntime(params.runtimeId);
159+
if (provider === OPENAI_PROVIDER_ID && runtimeId === "codex") {
160+
return OPENAI_CODEX_PROVIDER_ID;
161+
}
162+
return params.provider;
163+
}

src/agents/pi-embedded-runner/compact.queued.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import { resolveUserPath } from "../../utils.js";
2020
import { resolveAgentDir, resolveSessionAgentIds } from "../agent-scope.js";
2121
import { resolveContextWindowInfo } from "../context-window-guard.js";
2222
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
23-
import { maybeCompactAgentHarnessSession } from "../harness/selection.js";
23+
import {
24+
maybeCompactAgentHarnessSession,
25+
resolveAgentHarnessPolicy,
26+
} from "../harness/selection.js";
27+
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
2428
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
2529
import type { CompactEmbeddedPiSessionParams } from "./compact.types.js";
2630
import { asCompactionHookRunner, runPostCompactionSideEffects } from "./compaction-hooks.js";
@@ -83,9 +87,19 @@ export async function compactEmbeddedPiSession(
8387
params.config,
8488
);
8589
const ceRuntimeModel = ceModel as ProviderRuntimeModel | undefined;
90+
const ceHarnessPolicy = resolveAgentHarnessPolicy({
91+
provider: ceProvider,
92+
modelId: ceModelId,
93+
config: params.config,
94+
agentId: agentIds.sessionAgentId,
95+
sessionKey: params.sessionKey,
96+
});
8697
contextTokenBudget = resolveContextWindowInfo({
8798
cfg: params.config,
88-
provider: ceProvider,
99+
provider: resolveContextConfigProviderForRuntime({
100+
provider: ceProvider,
101+
runtimeId: ceHarnessPolicy.runtime,
102+
}),
89103
modelId: ceModelId,
90104
modelContextTokens: readPiModelContextTokens(ceModel),
91105
modelContextWindow: ceRuntimeModel?.contextWindow,

src/agents/pi-embedded-runner/compact.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../d
5858
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
5959
import { resolveOpenClawReferencePaths } from "../docs-path.js";
6060
import { coerceToFailoverError, describeFailoverError } from "../failover-error.js";
61+
import { resolveAgentHarnessPolicy } from "../harness/selection.js";
6162
import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-prompt.js";
6263
import {
6364
applyAuthHeaderOverride,
@@ -68,6 +69,7 @@ import {
6869
import { isFallbackSummaryError, runWithModelFallback } from "../model-fallback.js";
6970
import { supportsModelTools } from "../model-tool-support.js";
7071
import { ensureOpenClawModelsJson } from "../models-config.js";
72+
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
7173
import { createBundleLspToolRuntime } from "../pi-bundle-lsp-runtime.js";
7274
import { createBundleMcpToolRuntime } from "../pi-bundle-mcp-tools.js";
7375
import { ensureSessionHeader } from "../pi-embedded-helpers.js";
@@ -646,9 +648,19 @@ async function compactEmbeddedPiSessionDirectOnce(
646648
// Apply contextTokens cap to model so pi-coding-agent's auto-compaction
647649
// threshold uses the effective limit, not the native context window.
648650
const runtimeModelWithContext = runtimeModel as ProviderRuntimeModel;
651+
const runtimeHarnessPolicy = resolveAgentHarnessPolicy({
652+
provider,
653+
modelId,
654+
config: params.config,
655+
agentId: effectiveSkillAgentId,
656+
sessionKey: params.sessionKey,
657+
});
649658
const ctxInfo = resolveContextWindowInfo({
650659
cfg: params.config,
651-
provider,
660+
provider: resolveContextConfigProviderForRuntime({
661+
provider,
662+
runtimeId: runtimeHarnessPolicy.runtime,
663+
}),
652664
modelId,
653665
modelContextTokens: readPiModelContextTokens(runtimeModel),
654666
modelContextWindow: runtimeModelWithContext.contextWindow,

src/agents/pi-embedded-runner/run.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
shouldPreferExplicitConfigApiKeyAuth,
6363
} from "../model-auth.js";
6464
import { ensureOpenClawModelsJson } from "../models-config.js";
65+
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
6566
import {
6667
retireSessionMcpRuntime,
6768
retireSessionMcpRuntimeForSessionKey,
@@ -189,16 +190,6 @@ const COMPACTION_CONTINUATION_RETRY_INSTRUCTION =
189190
"The previous attempt compacted the conversation context before producing a final user-visible answer. Continue from the compacted transcript and produce the final answer now. Do not restart from scratch, do not repeat completed work, and do not rerun tools unless the transcript clearly lacks required evidence.";
190191
type EmbeddedRunAttemptForRunner = Awaited<ReturnType<typeof runEmbeddedAttemptWithBackend>>;
191192

192-
function resolveHarnessContextConfigProvider(params: {
193-
provider: string;
194-
harnessId: string;
195-
}): string {
196-
if (params.harnessId === "codex" && params.provider.trim().toLowerCase() === "openai") {
197-
return "openai-codex";
198-
}
199-
return params.provider;
200-
}
201-
202193
function resolveEmbeddedRunLaneTimeoutMs(timeoutMs: number): number | undefined {
203194
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
204195
return undefined;
@@ -597,9 +588,9 @@ export async function runEmbeddedPiAgent(
597588
const resolvedRuntimeModel = resolveEffectiveRuntimeModel({
598589
cfg: params.config,
599590
provider,
600-
contextConfigProvider: resolveHarnessContextConfigProvider({
591+
contextConfigProvider: resolveContextConfigProviderForRuntime({
601592
provider,
602-
harnessId: agentHarness.id,
593+
runtimeId: agentHarness.id,
603594
}),
604595
modelId,
605596
runtimeModel,

src/auto-reply/reply/commands-compact.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ vi.mock("./commands-compact.runtime.js", () => ({
2020
waitForEmbeddedPiRunEnd: vi.fn().mockResolvedValue(undefined),
2121
}));
2222

23-
const { compactEmbeddedPiSession, incrementCompactionCount, resolveSessionFilePathOptions } =
24-
await import("./commands-compact.runtime.js");
23+
const {
24+
compactEmbeddedPiSession,
25+
formatContextUsageShort,
26+
incrementCompactionCount,
27+
resolveSessionFilePathOptions,
28+
} = await import("./commands-compact.runtime.js");
2529
const { handleCompactCommand } = await import("./commands-compact.js");
2630

2731
function buildCompactParams(
@@ -342,4 +346,54 @@ describe("handleCompactCommand", () => {
342346
expect(call.sessionEntry.sessionId).toBe("target-session");
343347
expect(call.tokensAfter).toBe(321);
344348
});
349+
350+
it("resolves /compact context budget from the active Codex runtime config instead of stale session metadata", async () => {
351+
vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({
352+
ok: true,
353+
compacted: true,
354+
result: {
355+
summary: "compacted",
356+
firstKeptEntryId: "first-kept",
357+
tokensBefore: 199_000,
358+
tokensAfter: 56_000,
359+
},
360+
});
361+
362+
await handleCompactCommand(
363+
{
364+
...buildCompactParams("/compact", {
365+
agents: {
366+
defaults: {
367+
models: {
368+
"openai/gpt-5.5": {
369+
agentRuntime: { id: "codex" },
370+
},
371+
},
372+
},
373+
},
374+
commands: { text: true },
375+
channels: { whatsapp: { allowFrom: ["*"] } },
376+
models: {
377+
providers: {
378+
"openai-codex": {
379+
models: [{ id: "gpt-5.5", contextWindow: 258_000 }],
380+
},
381+
},
382+
},
383+
} as unknown as OpenClawConfig),
384+
provider: "openai",
385+
model: "openai/gpt-5.5",
386+
contextTokens: 0,
387+
sessionEntry: {
388+
sessionId: "live-session",
389+
updatedAt: Date.now(),
390+
contextTokens: 400_000,
391+
},
392+
} as HandleCommandsParams,
393+
true,
394+
);
395+
396+
expect(requireCompactEmbeddedPiSessionCall().contextTokenBudget).toBe(258_000);
397+
expect(vi.mocked(formatContextUsageShort)).toHaveBeenLastCalledWith(56_000, 258_000);
398+
});
345399
});

src/auto-reply/reply/commands-compact.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
2+
import { resolveContextTokensForModel } from "../../agents/context.js";
3+
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
4+
import {
5+
OPENAI_CODEX_PROVIDER_ID,
6+
OPENAI_PROVIDER_ID,
7+
resolveContextConfigProviderForRuntime,
8+
} from "../../agents/openai-codex-routing.js";
9+
import { normalizeProviderId } from "../../agents/provider-id.js";
210
import type { OpenClawConfig } from "../../config/types.openclaw.js";
311
import { logVerbose } from "../../globals.js";
412
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
@@ -75,6 +83,99 @@ function formatCompactionReason(reason?: string): string | undefined {
7583
return text;
7684
}
7785

86+
function resolveManualCompactContextTokenBudget(params: {
87+
cfg: OpenClawConfig;
88+
provider?: string;
89+
model?: string;
90+
agentId: string;
91+
sessionKey: string;
92+
liveContextTokens?: number;
93+
persistedContextTokens?: number;
94+
}): number | undefined {
95+
const liveContextTokens =
96+
typeof params.liveContextTokens === "number" &&
97+
Number.isFinite(params.liveContextTokens) &&
98+
params.liveContextTokens > 0
99+
? Math.floor(params.liveContextTokens)
100+
: undefined;
101+
102+
const model = normalizeOptionalString(params.model);
103+
const provider = normalizeOptionalString(params.provider);
104+
if (!model || !provider) {
105+
return liveContextTokens ?? resolvePersistedContextTokens(params.persistedContextTokens);
106+
}
107+
108+
const harnessPolicy = resolveAgentHarnessPolicy({
109+
provider,
110+
modelId: model,
111+
config: params.cfg,
112+
agentId: params.agentId,
113+
sessionKey: params.sessionKey,
114+
});
115+
const contextConfigProvider = resolveContextConfigProviderForRuntime({
116+
provider,
117+
runtimeId: harnessPolicy.runtime,
118+
});
119+
const configuredContextTokens = resolveContextTokensForModel({
120+
cfg: params.cfg,
121+
provider: contextConfigProvider,
122+
model: resolveManualCompactContextModelId({
123+
provider,
124+
contextConfigProvider,
125+
model,
126+
}),
127+
allowAsyncLoad: false,
128+
});
129+
if (typeof configuredContextTokens === "number" && configuredContextTokens > 0) {
130+
const configuredBudget = Math.floor(configuredContextTokens);
131+
return liveContextTokens !== undefined
132+
? Math.min(liveContextTokens, configuredBudget)
133+
: configuredBudget;
134+
}
135+
136+
if (liveContextTokens !== undefined) {
137+
return liveContextTokens;
138+
}
139+
140+
return resolvePersistedContextTokens(params.persistedContextTokens);
141+
}
142+
143+
function resolvePersistedContextTokens(value: number | undefined): number | undefined {
144+
return typeof value === "number" && Number.isFinite(value) && value > 0
145+
? Math.floor(value)
146+
: undefined;
147+
}
148+
149+
function resolveManualCompactContextModelId(params: {
150+
provider: string;
151+
contextConfigProvider: string;
152+
model: string;
153+
}): string {
154+
const model = params.model.trim();
155+
const slashIndex = model.indexOf("/");
156+
if (slashIndex <= 0) {
157+
return model;
158+
}
159+
160+
const modelProvider = normalizeProviderId(model.slice(0, slashIndex));
161+
const selectedProvider = normalizeProviderId(params.provider);
162+
const contextConfigProvider = normalizeProviderId(params.contextConfigProvider);
163+
const modelId = model.slice(slashIndex + 1).trim();
164+
if (!modelId) {
165+
return model;
166+
}
167+
168+
if (
169+
modelProvider === selectedProvider ||
170+
modelProvider === contextConfigProvider ||
171+
(modelProvider === OPENAI_PROVIDER_ID && contextConfigProvider === OPENAI_CODEX_PROVIDER_ID)
172+
) {
173+
return modelId;
174+
}
175+
176+
return model;
177+
}
178+
78179
export const handleCompactCommand: CommandHandler = async (params) => {
79180
const compactRequested =
80181
params.command.commandBodyNormalized === "/compact" ||
@@ -116,6 +217,15 @@ export const handleCompactCommand: CommandHandler = async (params) => {
116217
agentId: sessionAgentId,
117218
isGroup: params.isGroup,
118219
});
220+
const contextTokenBudget = resolveManualCompactContextTokenBudget({
221+
cfg: params.cfg,
222+
provider: params.provider,
223+
model: params.model,
224+
agentId: sessionAgentId,
225+
sessionKey: params.sessionKey,
226+
liveContextTokens: params.contextTokens,
227+
persistedContextTokens: targetSessionEntry.contextTokens,
228+
});
119229
const result = await runtime.compactEmbeddedPiSession({
120230
sessionId,
121231
sessionKey: params.sessionKey,
@@ -143,6 +253,7 @@ export const handleCompactCommand: CommandHandler = async (params) => {
143253
skillsSnapshot: targetSessionEntry.skillsSnapshot,
144254
provider: params.provider,
145255
model: params.model,
256+
contextTokenBudget,
146257
agentHarnessId:
147258
targetSessionEntry.sessionId === sessionId ? targetSessionEntry.agentHarnessId : undefined,
148259
thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()),
@@ -186,7 +297,7 @@ export const handleCompactCommand: CommandHandler = async (params) => {
186297
tokensAfterCompaction ?? runtime.resolveFreshSessionTotalTokens(targetSessionEntry);
187298
const contextSummary = runtime.formatContextUsageShort(
188299
typeof totalTokens === "number" && totalTokens > 0 ? totalTokens : null,
189-
params.contextTokens ?? targetSessionEntry.contextTokens ?? null,
300+
contextTokenBudget ?? null,
190301
);
191302
const reason = formatCompactionReason(result.reason);
192303
const line = reason

0 commit comments

Comments
 (0)