From a4cdaf1592478c8206f55b507b15410fc0e38e47 Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:37:26 +0300 Subject: [PATCH 1/2] fix(server): Codex forks no longer request the full thread history A fork of a very large Codex conversation returned the whole turn history and exceeded the transport limit, though the adapter only needs thread metadata. The fork request now sets excludeTurns, and the recorded fork frames in the replay fixtures were updated to match. Fixes #24460 --- .../Adapters/CodexAdapterV2.test.ts | 83 ++++++++++++++++++- .../Adapters/CodexAdapterV2.ts | 4 + .../codex_transcript.ndjson | 2 +- .../codex_transcript.ndjson | 4 +- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 69fca8c0825b..c52b6c2c9426 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -6606,7 +6606,11 @@ describe("CodexAdapterV2 post-settle continuation", () => { frame: { id: 3, method: "thread/fork", - params: { threadId: nativeThreadId, config: CodexAdapterV2.CODEX_THREAD_CONFIG }, + params: { + threadId: nativeThreadId, + excludeTurns: true, + config: CodexAdapterV2.CODEX_THREAD_CONFIG, + }, }, }, { @@ -6729,6 +6733,83 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); + it.effect("forks at a native turn without asking Codex for the historical turns", () => + Effect.gen(function* () { + const nativeThreadId = "exclude-turns-source-thread"; + const forkThreadId = "exclude-turns-fork-thread"; + const preamble = codexReplayPreamble({ + nativeThreadId, + nativeTurnId: "exclude-turns-source-turn", + prompt: "unused", + }); + // Replay matches the whole outbound frame, so a fork request that omits + // `excludeTurns` (the full-history payload that overran the transport + // limit) fails here instead of reaching a real app-server. + const transcript = makeCodexReplayTranscript({ + scenario: "codex-fork-exclude-turns", + entries: [ + ...preamble.slice(0, 5), + { + type: "expect_outbound", + label: "thread/fork", + frame: { + id: 3, + method: "thread/fork", + params: { + threadId: nativeThreadId, + lastTurnId: "native-turn-first", + excludeTurns: true, + config: CodexAdapterV2.CODEX_THREAD_CONFIG, + }, + }, + }, + { + type: "emit_inbound", + label: "thread/fork", + frame: { + id: 3, + result: codexReplayThreadResult({ + nativeThreadId: forkThreadId, + forkedFromId: nativeThreadId, + }), + }, + }, + ], + }); + const harness = yield* makeCodexReplayHarness(transcript); + const now = yield* DateTime.now; + const firstTurn = codexReplaySourceTurn({ + id: "provider-turn-first", + ordinal: 1, + nativeId: "native-turn-first", + providerThreadId: harness.providerThread.id, + now, + }); + const secondTurn = codexReplaySourceTurn({ + id: "provider-turn-second", + ordinal: 2, + nativeId: "native-turn-second", + providerThreadId: harness.providerThread.id, + now, + }); + + const forkedProviderThread = yield* harness.runtime.forkThread({ + sourceProviderThread: harness.providerThread, + sourceProviderTurns: [firstTurn, secondTurn], + providerTurnId: firstTurn.id, + targetThreadId: ThreadId.make("thread-fork-exclude-turns-target"), + }); + + assert.equal(forkedProviderThread.nativeThreadRef?.nativeId, forkThreadId); + assert.equal(forkedProviderThread.forkedFrom?.providerTurnId, firstTurn.id); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ); + describe("native goals", () => { const nativeThreadId = "goal-thread"; const objective = "Ship the feature"; diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 58cd4a0c0c17..b5681cf3426a 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -7640,6 +7640,10 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( Effect.andThen( client.request("thread/fork", { threadId, + // The adapter only needs thread metadata; without this the + // response carries the whole history, which overruns the + // transport limit on very large threads. + excludeTurns: true, ...(boundary.lastTurnId === undefined ? {} : { lastTurnId: boundary.lastTurnId }), diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/codex_transcript.ndjson index 93d16ba09578..c3fe8b38f92d 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/codex_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/codex_transcript.ndjson @@ -48,7 +48,7 @@ {"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":61,"windowDurationMins":300,"resetsAt":1777429830},"secondary":{"usedPercent":23,"windowDurationMins":10080,"resetsAt":1777959590},"credits":null,"planType":"unknown","rateLimitReachedType":null}}}} {"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","status":{"type":"idle"}}}} {"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turn":{"id":"019dd6ba-416d-7902-95cf-d4179c3f4738","items":[],"status":"completed","error":null,"startedAt":1777424023,"completedAt":1777424025,"durationMs":1596}}}} -{"type":"expect_outbound","label":"thread/fork","frame":{"id":5,"method":"thread/fork","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","lastTurnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","config":{"tools.update_plan.enabled":true,"mcp_optional_startup_grace_ms":10000}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":5,"method":"thread/fork","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","lastTurnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","excludeTurns":true,"config":{"tools.update_plan.enabled":true,"mcp_optional_startup_grace_ms":10000}}}} {"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} {"type":"emit_inbound","label":"warning","frame":{"method":"warning","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","message":"Under-development features enabled: apply_patch_freeform. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in /home/replay-user/.codex/config.toml."}}} {"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/codex_transcript.ndjson index 0674d949f141..60f1e9514856 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/codex_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/codex_transcript.ndjson @@ -27,7 +27,7 @@ {"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"01a0d610-155f-7391-843e-120bf1475357","turnId":"01a0d610-1619-7d91-bae2-ec4a7b899193","tokenUsage":{"total":{"totalTokens":17752,"inputTokens":17744,"cachedInputTokens":14848,"cacheWriteInputTokens":0,"outputTokens":8,"reasoningOutputTokens":0},"last":{"totalTokens":17752,"inputTokens":17744,"cachedInputTokens":14848,"cacheWriteInputTokens":0,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}},"emittedAtMs":1790297778003}} {"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"01a0d610-155f-7391-843e-120bf1475357","status":{"type":"idle"}},"emittedAtMs":1790297778011}} {"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"01a0d610-155f-7391-843e-120bf1475357","turn":{"id":"01a0d610-1619-7d91-bae2-ec4a7b899193","items":[{"type":"agentMessage","id":"msg_0c4d0b070a58ed52016ab5c6b1b0fc87d0a540ea4be4112eb0","text":"merge sibling source stored","phase":"final_answer","memoryCitation":null,"delivery":null,"questions":null}],"itemsView":"summary","status":"completed","error":null,"startedAt":1790297773,"completedAt":1790297778,"durationMs":4400}},"emittedAtMs":1790297778011}} -{"type":"expect_outbound","label":"thread/fork","frame":{"id":4,"method":"thread/fork","params":{"config":{"tools.update_plan.enabled":true,"mcp_optional_startup_grace_ms":10000},"cwd":"","lastTurnId":"01a0d610-1619-7d91-bae2-ec4a7b899193","model":"gpt-6-sol","threadId":"01a0d610-155f-7391-843e-120bf1475357"}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":4,"method":"thread/fork","params":{"config":{"tools.update_plan.enabled":true,"mcp_optional_startup_grace_ms":10000},"cwd":"","excludeTurns":true,"lastTurnId":"01a0d610-1619-7d91-bae2-ec4a7b899193","model":"gpt-6-sol","threadId":"01a0d610-155f-7391-843e-120bf1475357"}}} {"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"Full-history hydration is deprecated for paginated threads; use `excludeTurns: true`, then page with `thread/turns/list` and `thread/items/list`.","details":null},"emittedAtMs":1790297778016}} {"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"threadId":"01a0d610-155f-7391-843e-120bf1475357","name":"xcodebuildmcp","status":"ready","error":null,"failureReason":null},"emittedAtMs":1790297778263}} {"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"threadId":"01a0d610-2789-7bb3-a492-ad7fa8a7db9c","name":"codex_apps","status":"starting","error":null,"failureReason":null},"emittedAtMs":1790297778313}} @@ -52,7 +52,7 @@ {"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"01a0d610-2789-7bb3-a492-ad7fa8a7db9c","turnId":"01a0d610-28ad-7071-82a1-f55ea8178ae8","tokenUsage":{"total":{"totalTokens":39723,"inputTokens":39707,"cachedInputTokens":26624,"cacheWriteInputTokens":0,"outputTokens":16,"reasoningOutputTokens":0},"last":{"totalTokens":21971,"inputTokens":21963,"cachedInputTokens":11776,"cacheWriteInputTokens":0,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}},"emittedAtMs":1790297782125}} {"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"01a0d610-2789-7bb3-a492-ad7fa8a7db9c","status":{"type":"idle"}},"emittedAtMs":1790297782161}} {"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"01a0d610-2789-7bb3-a492-ad7fa8a7db9c","turn":{"id":"01a0d610-28ad-7071-82a1-f55ea8178ae8","items":[{"type":"agentMessage","id":"msg_0c4d0b070a58ed52016ab5c6b5b44c87d08e7e5612c5312499","text":"first merge sibling stored","phase":"final_answer","memoryCitation":null,"delivery":null,"questions":null}],"itemsView":"summary","status":"completed","error":null,"startedAt":1790297778,"completedAt":1790297782,"durationMs":3783}},"emittedAtMs":1790297782161}} -{"type":"expect_outbound","label":"thread/fork","frame":{"id":6,"method":"thread/fork","params":{"config":{"tools.update_plan.enabled":true,"mcp_optional_startup_grace_ms":10000},"cwd":"","lastTurnId":"01a0d610-1619-7d91-bae2-ec4a7b899193","model":"gpt-6-sol","threadId":"01a0d610-155f-7391-843e-120bf1475357"}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":6,"method":"thread/fork","params":{"config":{"tools.update_plan.enabled":true,"mcp_optional_startup_grace_ms":10000},"cwd":"","excludeTurns":true,"lastTurnId":"01a0d610-1619-7d91-bae2-ec4a7b899193","model":"gpt-6-sol","threadId":"01a0d610-155f-7391-843e-120bf1475357"}}} {"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"Full-history hydration is deprecated for paginated threads; use `excludeTurns: true`, then page with `thread/turns/list` and `thread/items/list`.","details":null},"emittedAtMs":1790297782170}} {"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"threadId":"01a0d610-37d1-7ba1-918f-136cfdbd9bfd","name":"xcodebuildmcp","status":"starting","error":null,"failureReason":null},"emittedAtMs":1790297782559}} {"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"threadId":"01a0d610-37d1-7ba1-918f-136cfdbd9bfd","name":"codex_apps","status":"starting","error":null,"failureReason":null},"emittedAtMs":1790297782559}} From a2cb50c1c6c33757e634de1467bfe304301ca3d4 Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:13:36 +0300 Subject: [PATCH 2/2] fix(server): replay recorder forks with excludeTurns too The adapter now sends excludeTurns on thread/fork, and the fixtures were updated to match. The recorder still built the request without it, so a re-recorded fixture would not match the adapter's frame. It now sends excludeTurns like its thread/resume requests do. --- apps/server/scripts/record-codex-app-server-replay-fixture.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/scripts/record-codex-app-server-replay-fixture.ts b/apps/server/scripts/record-codex-app-server-replay-fixture.ts index aa2c17af06c3..6320c8020ace 100644 --- a/apps/server/scripts/record-codex-app-server-replay-fixture.ts +++ b/apps/server/scripts/record-codex-app-server-replay-fixture.ts @@ -1431,6 +1431,7 @@ const runReplaySession = Effect.fn("record-codex-app-server-replay-fixture.runRe const lastTurnId = lastTurnIdByThread.get(sourceThreadId); const forked = yield* client.request("thread/fork", { threadId: sourceThreadId, + excludeTurns: true, ...(lastTurnId === undefined ? {} : { lastTurnId }), ...threadRuntimeParams, });