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 <altay@hey.com>
This commit is contained in:
wuqxuan
2026-07-13 11:34:47 +03:00
committed by GitHub
co-authored by Altay
parent 1eb3956355
commit 6739b9fa07
2 changed files with 54 additions and 1 deletions
@@ -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();
@@ -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