From 466bfbe78c205e2d06b9f85f0fb88ef00ca2cf07 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 07:57:15 +0200 Subject: [PATCH] fix(codex): cap native reserve for small windows --- .../src/app-server/startup-binding.test.ts | 35 +++++++++++++++++++ .../codex/src/app-server/startup-binding.ts | 12 ++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/extensions/codex/src/app-server/startup-binding.test.ts b/extensions/codex/src/app-server/startup-binding.test.ts index fda0cff125f..0817a44c354 100644 --- a/extensions/codex/src/app-server/startup-binding.test.ts +++ b/extensions/codex/src/app-server/startup-binding.test.ts @@ -112,6 +112,41 @@ describe("Codex app-server startup binding", () => { expect(savedBinding).toBeUndefined(); }); + it("caps the default native reserve so small context windows keep prompt budget", async () => { + const sessionFile = path.join(tempDir, "session.jsonl"); + const workspaceDir = path.join(tempDir, "workspace"); + const agentDir = path.join(tempDir, "agent"); + await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); + await writeSessionRecord(sessionFile, { totalTokens: 100 }); + const rolloutDir = path.join(agentDir, "codex-home", "sessions"); + await fs.mkdir(rolloutDir, { recursive: true }); + await fs.writeFile( + path.join(rolloutDir, "rollout-thread-existing.jsonl"), + `${JSON.stringify({ + payload: { + type: "token_count", + info: { + last_token_usage: { + total_tokens: 100, + }, + model_context_window: 16_000, + }, + }, + })}\n`, + ); + + const binding = await rotateOversizedCodexAppServerStartupBinding({ + binding: await readCodexAppServerBinding(sessionFile), + sessionFile, + agentDir, + config: undefined, + }); + + expect(binding?.threadId).toBe("thread-existing"); + const savedBinding = await readCodexAppServerBinding(sessionFile); + expect(savedBinding?.threadId).toBe("thread-existing"); + }); + it("honors shorthand byte units for native rollout limits", async () => { const sessionFile = path.join(tempDir, "session.jsonl"); const workspaceDir = path.join(tempDir, "workspace"); diff --git a/extensions/codex/src/app-server/startup-binding.ts b/extensions/codex/src/app-server/startup-binding.ts index 1d504cd2a44..da568e562e0 100644 --- a/extensions/codex/src/app-server/startup-binding.ts +++ b/extensions/codex/src/app-server/startup-binding.ts @@ -13,6 +13,8 @@ import { clearCodexAppServerBinding, type CodexAppServerThreadBinding } from "./ // thread that is already too close to the server-side window for the next turn. const CODEX_APP_SERVER_NATIVE_THREAD_FALLBACK_MAX_TOKENS = 300_000; const CODEX_APP_SERVER_NATIVE_THREAD_DEFAULT_RESERVE_TOKENS = 20_000; +const CODEX_APP_SERVER_NATIVE_THREAD_MIN_PROMPT_BUDGET_TOKENS = 8_000; +const CODEX_APP_SERVER_NATIVE_THREAD_MIN_PROMPT_BUDGET_RATIO = 0.5; const CODEX_APP_SERVER_BYTE_UNITS: Record = { b: 1, k: 1024, @@ -250,7 +252,15 @@ function resolveCodexAppServerNativeThreadTokenFuse(params: { : 0; const contextWindow = params.modelContextWindow ?? CODEX_APP_SERVER_NATIVE_THREAD_FALLBACK_MAX_TOKENS; - return Math.max(1, contextWindow - params.reserveTokens - projectedTurnTokens); + const minPromptBudget = Math.min( + CODEX_APP_SERVER_NATIVE_THREAD_MIN_PROMPT_BUDGET_TOKENS, + Math.max(1, Math.floor(contextWindow * CODEX_APP_SERVER_NATIVE_THREAD_MIN_PROMPT_BUDGET_RATIO)), + ); + const effectiveReserveTokens = Math.min( + params.reserveTokens, + Math.max(0, contextWindow - minPromptBudget), + ); + return Math.max(1, contextWindow - effectiveReserveTokens - projectedTurnTokens); } function maxFiniteNumber(values: Array): number | undefined {