From 6739b9fa0747f1dcc0f3468ede99a13eb96ec65a Mon Sep 17 00:00:00 2001 From: wuqxuan Date: Mon, 13 Jul 2026 16:34:47 +0800 Subject: [PATCH] fix(codex): do not promote missing_tool_result on abort/interrupt (#104955) * fix(codex): do not promote missing_tool_result on abort/interrupt Keep synthetic tool.result rows for transcript integrity, but skip promoting the missing-tool invariant to promptError when the turn is already aborted or interrupted so mid-exec cancel paths keep abort classification and assistant text (#104898). * fix(codex): preserve explicit abort outcome --------- Co-authored-by: Altay --- .../src/app-server/event-projector.test.ts | 50 +++++++++++++++++++ .../codex/src/app-server/event-projector.ts | 5 +- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/extensions/codex/src/app-server/event-projector.test.ts b/extensions/codex/src/app-server/event-projector.test.ts index 4ccdf686afa6..bc860f6bcdfc 100644 --- a/extensions/codex/src/app-server/event-projector.test.ts +++ b/extensions/codex/src/app-server/event-projector.test.ts @@ -274,6 +274,24 @@ function turnWithStatus(status: string, items: unknown[] = []): ProjectorNotific } as ProjectorNotification; } +function pendingCommandStarted(id: string): ProjectorNotification { + return forCurrentTurn("item/started", { + item: { + type: "commandExecution", + id, + command: "/bin/bash -lc 'sleep 600'", + cwd: "/workspace", + processId: null, + source: "agent", + status: "inProgress", + commandActions: [], + aggregatedOutput: null, + exitCode: null, + durationMs: null, + }, + }); +} + describe("CodexAppServerEventProjector", () => { it("projects assistant deltas and usage into embedded attempt results", async () => { const { onAssistantMessageStart, onPartialReply, projector } = @@ -1651,6 +1669,38 @@ describe("CodexAppServerEventProjector", () => { expect(result.assistantTexts).toEqual([]); }); + it("keeps missing tool detail without overriding an explicit abort", async () => { + const projector = await createProjector(); + projector.markAborted(); + + await projector.handleNotification(pendingCommandStarted("cmd-aborted")); + await projector.handleNotification(turnWithStatus("interrupted")); + + const result = projector.buildResult(buildEmptyToolTelemetry()); + + expect(result.aborted).toBe(true); + expect(result.promptError).toBeNull(); + expect(result.promptErrorSource).toBeNull(); + expect(result.lastToolError).toMatchObject({ + toolName: "bash", + error: expect.stringContaining("without a matching tool.result"), + }); + }); + + it("fails closed when interrupted status has no abort marker", async () => { + const projector = await createProjector(); + + await projector.handleNotification(pendingCommandStarted("cmd-interrupted")); + await projector.handleNotification(turnWithStatus("interrupted")); + + const result = projector.buildResult(buildEmptyToolTelemetry()); + + expect(result.aborted).toBe(false); + expect(result.promptError).toContain("without a matching tool.result"); + expect(result.promptErrorSource).toBe("prompt"); + expect(result.lastToolError).toBeUndefined(); + }); + it("does not fail a completed reply after a retryable app-server error notification", async () => { const projector = await createProjector(); diff --git a/extensions/codex/src/app-server/event-projector.ts b/extensions/codex/src/app-server/event-projector.ts index 99a83552a563..db0d59fc011d 100644 --- a/extensions/codex/src/app-server/event-projector.ts +++ b/extensions/codex/src/app-server/event-projector.ts @@ -784,7 +784,10 @@ export class CodexAppServerEventProjector { assistantTexts.some((text) => text.trim().length > 0); this.synthesizeMissingToolResults({ synthesize: legacyFailClosed, - recordPromptError: legacyFailClosed && !hasDeliverableAssistantOnCompletedTurn, + recordPromptError: + legacyFailClosed && + !hasDeliverableAssistantOnCompletedTurn && + !this.aborted, }); const lastAssistant = assistantTexts.length > 0