From e6113f2606c0717feab142d906c1e995afaabe6e Mon Sep 17 00:00:00 2001 From: luke2x <97411838+luke2x@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:31:22 +0100 Subject: [PATCH 1/6] fix(codex): settle compaction items when turns end --- apps/mobile/src/lib/threadActivity.test.ts | 6 +- .../Adapters/CodexAdapterV2.test.ts | 136 ++++++++++++++++++ .../Adapters/CodexAdapterV2.ts | 41 +++++- .../src/work-log/presentation.test.ts | 21 +++ .../src/work-log/presentation.ts | 8 +- 5 files changed, 202 insertions(+), 10 deletions(-) diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts index d88e840ba40f..3a50fc9cf0fc 100644 --- a/apps/mobile/src/lib/threadActivity.test.ts +++ b/apps/mobile/src/lib/threadActivity.test.ts @@ -1576,7 +1576,7 @@ describe("retained v2 feed presentation", () => { expect(afterPresentation[1]).toBe(beforePresentation[1]); }); - it.each(["running", "completed", "interrupted"] as const)( + it.each(["running", "completed", "failed", "interrupted", "cancelled"] as const)( "uses the compaction row as the live activity only while %s", (status) => { const compact = projected( @@ -1612,7 +1612,9 @@ describe("retained v2 feed presentation", () => { ? "Compacting context" : status === "completed" ? "Context compacted 899K → 19K tokens" - : "Context compacted", + : status === "failed" + ? "Context compaction failed" + : "Context compaction stopped", }, ], }); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 862810662310..2f9671f7ecfd 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -2850,6 +2850,142 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); + it.effect.each([ + { status: "failed", completeItem: false }, + { status: "interrupted", completeItem: false }, + { status: "failed", completeItem: true }, + ] as const)( + "settles automatic compaction on $status (item completed: $completeItem)", + ({ status, completeItem }) => + Effect.scoped( + Effect.gen(function* () { + const nativeThreadId = `compact-${status}-thread`; + const nativeTurnId = `compact-${status}-turn`; + const item = { type: "contextCompaction", id: `compact-${status}-item` }; + const error = { + message: + "Error running remote compact task: stream disconnected before completion: stream closed before response.completed", + codexErrorInfo: "other", + additionalDetails: null, + }; + const transcript = makeCodexReplayTranscript({ + scenario: `automatic-compaction-${status}`, + entries: [ + ...codexReplayPreamble({ nativeThreadId, nativeTurnId, prompt: "Continue." }), + { + type: "emit_inbound", + label: "item/started/compaction", + frame: { + method: "item/started", + params: { + threadId: nativeThreadId, + turnId: nativeTurnId, + item, + startedAtMs: 1782622441000, + }, + }, + }, + ...([1, 2] as const).map((attempt) => ({ + type: "emit_inbound" as const, + label: `error/retry-${attempt}`, + frame: { + method: "error", + params: { + threadId: nativeThreadId, + turnId: nativeTurnId, + willRetry: true, + error: { + message: `Reconnecting... ${attempt}/2`, + additionalDetails: "stream disconnected before completion", + codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: null } }, + }, + }, + }, + })), + ...(completeItem + ? [ + { + type: "emit_inbound" as const, + label: "item/completed/compaction", + frame: { + method: "item/completed", + params: { + threadId: nativeThreadId, + turnId: nativeTurnId, + item, + completedAtMs: 1782622444000, + }, + }, + }, + ] + : []), + ...(status === "failed" + ? [ + { + type: "emit_inbound" as const, + label: "error/terminal", + frame: { + method: "error", + params: { + threadId: nativeThreadId, + turnId: nativeTurnId, + willRetry: false, + error, + }, + }, + }, + ] + : []), + { + type: "emit_inbound", + label: "turn/completed", + frame: { + method: "turn/completed", + params: { + threadId: nativeThreadId, + turn: { + ...makeCodexReplayTurn({ id: nativeTurnId, status }), + error: status === "failed" ? error : null, + }, + }, + }, + }, + ], + }); + const harness = yield* makeCodexReplayHarness(transcript); + yield* harness.runtime.startTurn( + makeCodexTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make(`compact-${status}-attempt`), + text: "Continue.", + }), + ); + yield* harness.firstTerminal; + const items = harness.events.flatMap((event) => + event.type === "turn_item.updated" && event.turnItem.type === "compaction" + ? [event.turnItem] + : [], + ); + assert.deepEqual( + items.map((entry) => entry.status), + ["running", completeItem ? "completed" : status], + ); + assert.equal(items[0]?.id, items[1]?.id); + assert.deepEqual(items[1]?.startedAt, DateTime.makeUnsafe(1782622441000)); + assert.deepEqual( + items[1]?.completedAt, + DateTime.makeUnsafe(completeItem ? 1782622444000 : 1782622450000), + ); + assert.equal(harness.terminalEvents()[0]?.status, status); + if (status === "failed") { + assert.equal(harness.terminalEvents()[0]?.failure?.message, error.message); + } + }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), + ), + ); + it.effect("resumes a provider thread without requesting or decoding its history", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index fefc1ece4fc9..7feb646a9fb8 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1144,6 +1144,7 @@ interface ActiveCodexTurnContext { readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; readonly subagent: CodexSubagentThreadContext | null; readonly startedAt: DateTime.Utc; + readonly runningCompactionItemIds: Set; // Item positions allocated in this turn. Later turns never look items up // here: late background items resolve their settled turn's context, and a // subagent's approvals allocate on the owning root turn. @@ -1994,6 +1995,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi subagent: null, startedAt: input.startedAt, itemPositions: new Map(), + runningCompactionItemIds: new Set(), }; yield* Ref.update(limitedTurnItems, (current) => { const next = new Map(current); @@ -2584,6 +2586,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi subagent, startedAt: turn.startedAt, itemPositions: new Map(), + runningCompactionItemIds: new Set(), }; beginTurnTokenUsage(activeContext); yield* Ref.update(activeTurns, (current) => { @@ -4559,15 +4562,17 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi const emitCompactionItem = Effect.fn("CodexAdapterV2.emitCompactionItem")(function* ( context: ActiveCodexTurnContext, nativeItemId: string, - status: "running" | "completed", - nativeStartedAt?: DateTime.Utc, + status: "running" | "completed" | "failed" | "interrupted" | "cancelled", + nativeTimestamp?: DateTime.Utc, ) { - const now = yield* DateTime.now; + const now = nativeTimestamp ?? (yield* DateTime.now); const { ordinal, startedAt } = yield* resolveItemPosition( context, nativeItemId, - nativeStartedAt, + nativeTimestamp, ); + if (status === "running") context.runningCompactionItemIds.add(nativeItemId); + else context.runningCompactionItemIds.delete(nativeItemId); yield* emitProviderEvent({ type: "turn_item.updated", driver: CODEX_PROVIDER, @@ -4587,9 +4592,16 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi type: "compaction", driver: CODEX_PROVIDER, status, - title: status === "completed" ? "Context compacted" : "Compacting context", + title: + status === "running" + ? "Compacting context" + : status === "completed" + ? "Context compacted" + : status === "failed" + ? "Context compaction failed" + : "Context compaction stopped", startedAt, - completedAt: status === "completed" ? now : null, + completedAt: status === "running" ? null : now, updatedAt: now, }, }); @@ -4756,7 +4768,12 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi } if (payload.item.type === "contextCompaction") { - yield* emitCompactionItem(context, payload.item.id, "completed"); + yield* emitCompactionItem( + context, + payload.item.id, + "completed", + DateTime.makeUnsafe(payload.completedAtMs), + ); return; } @@ -5762,6 +5779,15 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi for (const [key, part] of reasoningParts) { if (part.turnId === input.nativeTurnId) reasoningParts.delete(key); } + // Failed compaction can end the turn without item/completed. + for (const nativeItemId of input.context.runningCompactionItemIds) { + yield* emitCompactionItem( + input.context, + nativeItemId, + providerTurnStatusToTerminal(input.status), + input.completedAt, + ); + } const nativeThreadId = input.context.providerThread.nativeThreadRef?.nativeId; // Codex continues an active goal with another turn, so the run stays // open and this turn reads as running until that turn starts. @@ -6024,6 +6050,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi subagent: null, startedAt: now, itemPositions: new Map(), + runningCompactionItemIds: new Set(), }; const providerTurn = { id: context.providerTurnId, diff --git a/packages/client-runtime/src/work-log/presentation.test.ts b/packages/client-runtime/src/work-log/presentation.test.ts index 6e41a53478a1..e780a311b022 100644 --- a/packages/client-runtime/src/work-log/presentation.test.ts +++ b/packages/client-runtime/src/work-log/presentation.test.ts @@ -6,6 +6,7 @@ import { T3_MCP_TOOL_NAMES } from "@t3tools/shared/t3McpToolPresentation"; import { commandDetailRepeatsCommand, + contextCompactionLabel, extractCommandOutputText, liveThoughtLine, resolveViewedImageAsset, @@ -47,6 +48,26 @@ function commandItem( }; } +describe("contextCompactionLabel", () => { + it.each([ + ["pending", "Compacting context"], + ["running", "Compacting context"], + ["waiting", "Compacting context"], + ["failed", "Context compaction failed"], + ["cancelled", "Context compaction stopped"], + ["interrupted", "Context compaction stopped"], + ["completed", "Context compacted 20K → 5K tokens"], + ] as const)("shows the outcome for %s even when token counts are present", (status, label) => { + expect( + contextCompactionLabel({ status, beforeTokenCount: 20_000, afterTokenCount: 5_000 }), + ).toBe(label); + }); + + it("shows successful compaction without token counts", () => { + expect(contextCompactionLabel({ status: "completed" })).toBe("Context compacted"); + }); +}); + describe("workEntryIndicatesToolFailure", () => { const base = { id: "w1", diff --git a/packages/client-runtime/src/work-log/presentation.ts b/packages/client-runtime/src/work-log/presentation.ts index 8ef6f58be595..0e2e06881e74 100644 --- a/packages/client-runtime/src/work-log/presentation.ts +++ b/packages/client-runtime/src/work-log/presentation.ts @@ -55,7 +55,13 @@ export function contextCompactionLabel( "status" | "beforeTokenCount" | "afterTokenCount" >, ): string { - if (item.status === "running") return "Compacting context"; + if (item.status === "failed") return "Context compaction failed"; + if (item.status === "cancelled" || item.status === "interrupted") { + return "Context compaction stopped"; + } + if (item.status === "pending" || item.status === "running" || item.status === "waiting") { + return "Compacting context"; + } if (item.beforeTokenCount !== undefined && item.afterTokenCount !== undefined) { return `Context compacted ${formatTokens(item.beforeTokenCount)} → ${formatTokens(item.afterTokenCount)} tokens`; } From 0f64d43e5b39dc9de70e3463f85f3031c4583bb7 Mon Sep 17 00:00:00 2001 From: luke2x <97411838+luke2x@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:13:27 +0100 Subject: [PATCH 2/6] test(codex): provide MCP provider sessions to compaction replay tests Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/orchestration-v2/Adapters/CodexAdapterV2.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index d59bd20250d7..0bd7f5319a05 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -3045,7 +3045,11 @@ describe("CodexAdapterV2 post-settle continuation", () => { if (status === "failed") { assert.equal(harness.terminalEvents()[0]?.failure?.message, error.message); } - }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), ), ); From 181527cccaa8f6e382452ab23ba136f49ac74fca Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 10:08:41 +0000 Subject: [PATCH 3/6] test(codex): focus compaction terminal state regressions --- apps/mobile/src/lib/threadActivity.test.ts | 4 +- .../Adapters/CodexAdapterV2.test.ts | 153 +++++++++--------- .../src/work-log/presentation.test.ts | 21 --- 3 files changed, 77 insertions(+), 101 deletions(-) diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts index b96e519a18df..18c9e1db5992 100644 --- a/apps/mobile/src/lib/threadActivity.test.ts +++ b/apps/mobile/src/lib/threadActivity.test.ts @@ -1740,7 +1740,7 @@ describe("retained v2 feed presentation", () => { expect(afterPresentation[1]).toBe(beforePresentation[1]); }); - it.each(["running", "completed", "failed", "interrupted", "cancelled"] as const)( + it.each(["running", "completed", "failed", "interrupted"] as const)( "uses the compaction row as the live activity only while %s", (status) => { const compact = projected( @@ -1750,7 +1750,7 @@ describe("retained v2 feed presentation", () => { status, driver: null, beforeTokenCount: 899_000, - ...(status === "completed" ? { afterTokenCount: 19_000 } : {}), + afterTokenCount: 19_000, }, 1, ); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index ffb1e32bcfde..abab750cc4fd 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -3161,136 +3161,133 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); - it.effect.each([ - { status: "failed", completeItem: false }, - { status: "interrupted", completeItem: false }, - { status: "failed", completeItem: true }, - ] as const)( - "settles automatic compaction on $status (item completed: $completeItem)", - ({ status, completeItem }) => + it.effect.each(["failed", "interrupted", "Stop"] as const)( + "settles unfinished compaction on %s without changing completed compaction", + (outcome) => Effect.scoped( Effect.gen(function* () { - const nativeThreadId = `compact-${status}-thread`; - const nativeTurnId = `compact-${status}-turn`; - const item = { type: "contextCompaction", id: `compact-${status}-item` }; + const nativeThreadId = "compact-thread"; + const nativeTurnId = "compact-turn"; const error = { - message: - "Error running remote compact task: stream disconnected before completion: stream closed before response.completed", + message: "Remote compaction stream disconnected", codexErrorInfo: "other", additionalDetails: null, }; const transcript = makeCodexReplayTranscript({ - scenario: `automatic-compaction-${status}`, + scenario: `automatic-compaction-${outcome}`, entries: [ ...codexReplayPreamble({ nativeThreadId, nativeTurnId, prompt: "Continue." }), - { - type: "emit_inbound", - label: "item/started/compaction", - frame: { - method: "item/started", - params: { - threadId: nativeThreadId, - turnId: nativeTurnId, - item, - startedAtMs: 1782622441000, - }, - }, - }, - ...([1, 2] as const).map((attempt) => ({ + ...( + [ + { method: "item/started", id: "completed-item", startedAtMs: 1782622441000 }, + { method: "item/completed", id: "completed-item", completedAtMs: 1782622442000 }, + { method: "item/started", id: "unfinished-item", startedAtMs: 1782622443000 }, + ] as const + ).map(({ method, id, ...timestamps }) => ({ type: "emit_inbound" as const, - label: `error/retry-${attempt}`, + label: `${method}/${id}`, frame: { - method: "error", + method, params: { threadId: nativeThreadId, turnId: nativeTurnId, - willRetry: true, - error: { - message: `Reconnecting... ${attempt}/2`, - additionalDetails: "stream disconnected before completion", - codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: null } }, - }, + item: { type: "contextCompaction", id }, + ...timestamps, }, }, })), - ...(completeItem + ...(outcome === "Stop" ? [ { - type: "emit_inbound" as const, - label: "item/completed/compaction", + type: "expect_outbound" as const, + label: "turn/interrupt", frame: { - method: "item/completed", - params: { - threadId: nativeThreadId, - turnId: nativeTurnId, - item, - completedAtMs: 1782622444000, - }, + id: 4, + method: "turn/interrupt", + params: { threadId: nativeThreadId, turnId: nativeTurnId }, }, }, + { + type: "emit_inbound" as const, + label: "turn/interrupt/failed", + frame: { id: 4, error: { code: -32_000, message: "Interrupt failed" } }, + }, ] - : []), - ...(status === "failed" - ? [ + : [ { type: "emit_inbound" as const, - label: "error/terminal", + label: "turn/completed", frame: { - method: "error", + method: "turn/completed", params: { threadId: nativeThreadId, - turnId: nativeTurnId, - willRetry: false, - error, + turn: { + ...makeCodexReplayTurn({ id: nativeTurnId, status: outcome }), + error: outcome === "failed" ? error : null, + }, }, }, }, - ] - : []), - { - type: "emit_inbound", - label: "turn/completed", - frame: { - method: "turn/completed", - params: { - threadId: nativeThreadId, - turn: { - ...makeCodexReplayTurn({ id: nativeTurnId, status }), - error: status === "failed" ? error : null, - }, - }, - }, - }, + ]), ], }); - const harness = yield* makeCodexReplayHarness(transcript); + const compactionStarted = yield* Deferred.make(); + const harness = yield* makeCodexReplayHarness(transcript, (event) => + event.type === "turn_item.updated" && + event.turnItem.nativeItemRef?.nativeId === "unfinished-item" && + event.turnItem.status === "running" && + event.turnItem.providerTurnId !== null + ? Deferred.succeed(compactionStarted, event.turnItem.providerTurnId) + : Effect.void, + ); + yield* TestClock.setTime(1782622445000); yield* harness.runtime.startTurn( makeCodexTestTurnInput({ threadId: harness.threadId, providerThread: harness.providerThread, now: yield* DateTime.now, - attemptId: RunAttemptId.make(`compact-${status}-attempt`), + attemptId: RunAttemptId.make(`compact-${outcome}-attempt`), text: "Continue.", }), ); + if (outcome === "Stop") { + const providerTurnId = yield* Deferred.await(compactionStarted); + const interrupt = yield* harness.runtime + .interruptTurn({ providerThread: harness.providerThread, providerTurnId }) + .pipe(Effect.exit); + assert.equal(interrupt._tag, "Failure"); + } yield* harness.firstTerminal; const items = harness.events.flatMap((event) => event.type === "turn_item.updated" && event.turnItem.type === "compaction" ? [event.turnItem] : [], ); + const completed = items.filter( + (item) => item.nativeItemRef?.nativeId === "completed-item", + ); + const unfinished = items.filter( + (item) => item.nativeItemRef?.nativeId === "unfinished-item", + ); + const status = outcome === "failed" ? "failed" : "interrupted"; + assert.deepEqual( + unfinished.map((item) => item.status), + ["running", status], + ); + assert.equal(unfinished[0]?.id, unfinished[1]?.id); + assert.deepEqual(unfinished[1]?.startedAt, DateTime.makeUnsafe(1782622443000)); assert.deepEqual( - items.map((entry) => entry.status), - ["running", completeItem ? "completed" : status], + unfinished[1]?.completedAt, + DateTime.makeUnsafe(outcome === "Stop" ? 1782622445000 : 1782622450000), ); - assert.equal(items[0]?.id, items[1]?.id); - assert.deepEqual(items[1]?.startedAt, DateTime.makeUnsafe(1782622441000)); assert.deepEqual( - items[1]?.completedAt, - DateTime.makeUnsafe(completeItem ? 1782622444000 : 1782622450000), + completed.map((item) => item.status), + ["running", "completed"], ); + assert.deepEqual(completed[1]?.startedAt, DateTime.makeUnsafe(1782622441000)); + assert.deepEqual(completed[1]?.completedAt, DateTime.makeUnsafe(1782622442000)); assert.equal(harness.terminalEvents()[0]?.status, status); - if (status === "failed") { + if (outcome === "failed") { assert.equal(harness.terminalEvents()[0]?.failure?.message, error.message); } }).pipe( diff --git a/packages/client-runtime/src/work-log/presentation.test.ts b/packages/client-runtime/src/work-log/presentation.test.ts index 76c25360074e..e5f0acd9d9e3 100644 --- a/packages/client-runtime/src/work-log/presentation.test.ts +++ b/packages/client-runtime/src/work-log/presentation.test.ts @@ -6,7 +6,6 @@ import { T3_MCP_TOOL_NAMES } from "@t3tools/shared/t3McpToolPresentation"; import { commandDetailRepeatsCommand, - contextCompactionLabel, extractCommandOutputText, liveThoughtLine, resolveViewedImageAsset, @@ -48,26 +47,6 @@ function commandItem( }; } -describe("contextCompactionLabel", () => { - it.each([ - ["pending", "Compacting context"], - ["running", "Compacting context"], - ["waiting", "Compacting context"], - ["failed", "Context compaction failed"], - ["cancelled", "Context compaction stopped"], - ["interrupted", "Context compaction stopped"], - ["completed", "Context compacted 20K → 5K tokens"], - ] as const)("shows the outcome for %s even when token counts are present", (status, label) => { - expect( - contextCompactionLabel({ status, beforeTokenCount: 20_000, afterTokenCount: 5_000 }), - ).toBe(label); - }); - - it("shows successful compaction without token counts", () => { - expect(contextCompactionLabel({ status: "completed" })).toBe("Context compacted"); - }); -}); - describe("workEntryIndicatesToolFailure", () => { const base = { id: "w1", From df5f1cf284c6b0355b3699fc0a568c4389d8f924 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 10:12:46 +0000 Subject: [PATCH 4/6] fix(codex): simplify compaction finalization --- .../src/orchestration-v2/Adapters/CodexAdapterV2.ts | 12 ++++-------- packages/client-runtime/src/work-log/presentation.ts | 5 ++--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 501ff443609f..0bf6b25ef36e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1127,9 +1127,6 @@ interface ActiveCodexTurnContext { readonly subagent: CodexSubagentThreadContext | null; readonly startedAt: DateTime.Utc; readonly runningCompactionItemIds: Set; - // Item positions allocated in this turn. Later turns never look items up - // here: late background items resolve their settled turn's context, and a - // subagent's approvals allocate on the owning root turn. readonly itemPositions: Map< string, { readonly ordinal: number; readonly startedAt: DateTime.Utc } @@ -4581,13 +4578,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( context: ActiveCodexTurnContext, nativeItemId: string, status: "running" | "completed" | "failed" | "interrupted" | "cancelled", - nativeTimestamp?: DateTime.Utc, + updatedAt: DateTime.Utc, ) { - const now = nativeTimestamp ?? (yield* DateTime.now); const { ordinal, startedAt } = yield* resolveItemPosition( context, nativeItemId, - nativeTimestamp, + updatedAt, ); if (status === "running") context.runningCompactionItemIds.add(nativeItemId); else context.runningCompactionItemIds.delete(nativeItemId); @@ -4619,8 +4615,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ? "Context compaction failed" : "Context compaction stopped", startedAt, - completedAt: status === "running" ? null : now, - updatedAt: now, + completedAt: status === "running" ? null : updatedAt, + updatedAt, }, }); }); diff --git a/packages/client-runtime/src/work-log/presentation.ts b/packages/client-runtime/src/work-log/presentation.ts index d047f04b1f0b..b8a1d529d10a 100644 --- a/packages/client-runtime/src/work-log/presentation.ts +++ b/packages/client-runtime/src/work-log/presentation.ts @@ -1,4 +1,5 @@ import { + isOrchestrationV2WorkActive, isToolLifecycleItemType, type AssetResource, type RuntimeItemStatus, @@ -71,9 +72,7 @@ export function contextCompactionLabel(item: CompactionLabelItem): string { if (item.status === "cancelled" || item.status === "interrupted") { return "Context compaction stopped"; } - if (item.status === "pending" || item.status === "running" || item.status === "waiting") { - return "Compacting context"; - } + if (isOrchestrationV2WorkActive(item.status)) return "Compacting context"; const tokens = contextCompactionTokenCounts(item); return tokens ? `Context compacted ${tokens.before} → ${tokens.after} tokens` From b1dd2831f629362affd53020e7baa3a33c80db00 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 20 Oct 2026 04:45:15 +0000 Subject: [PATCH 5/6] fix(codex): retain main file-change tracking in merge --- .../Adapters/CodexAdapterV2.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index a6c7d9c46eca..f9810ac7ce34 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -4718,6 +4718,14 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ); } + if (payload.item.type === "fileChange") { + context.fileChangeKindsByItem.set( + payload.item.id, + Object.fromEntries(payload.item.changes.map(({ path, kind }) => [path, kind])), + ); + return; + } + if (payload.item.type === "agentMessage") { if (payload.item.phase !== "commentary") { yield* Ref.update(finalAnswerItemIdsByTurn, (current) => { @@ -4939,6 +4947,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( } if (payload.item.type === "fileChange") { + context.fileChangeKindsByItem.delete(payload.item.id); const artifacts = yield* buildFileChangeArtifacts(context, payload.item); if (artifacts === null) { return; @@ -5191,7 +5200,11 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( nativeItemId: payload.itemId, nativeRequestId: payload.itemId, requestKind: "file-change", - prompt: codexFileChangeApprovalPrompt(payload) ?? null, + prompt: + codexFileChangeApprovalPrompt({ + ...payload, + fileChanges: context.fileChangeKindsByItem.get(payload.itemId) ?? {}, + }) ?? null, }); const decision = yield* Deferred.make(); yield* Ref.update(pendingRuntimeRequests, (current) => { @@ -5472,7 +5485,11 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( nativeItemId: payload.callId, nativeRequestId: payload.callId, requestKind: "file-change", - prompt: codexFileChangeApprovalPrompt(payload) ?? null, + prompt: + codexFileChangeApprovalPrompt({ + ...payload, + fileChanges: context.fileChangeKindsByItem.get(payload.itemId) ?? {}, + }) ?? null, }); const decision = yield* Deferred.make(); yield* Ref.update(pendingRuntimeRequests, (current) => { From cb988578183cb368a3ed65884e8b11bbbe401c3a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 20 Oct 2026 04:46:27 +0000 Subject: [PATCH 6/6] fix(codex): preserve legacy patch approval parameters --- apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index f9810ac7ce34..5664fdb230da 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -5485,11 +5485,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( nativeItemId: payload.callId, nativeRequestId: payload.callId, requestKind: "file-change", - prompt: - codexFileChangeApprovalPrompt({ - ...payload, - fileChanges: context.fileChangeKindsByItem.get(payload.itemId) ?? {}, - }) ?? null, + prompt: codexFileChangeApprovalPrompt(payload) ?? null, }); const decision = yield* Deferred.make(); yield* Ref.update(pendingRuntimeRequests, (current) => {