From 9eaf72ab3177017d22264752a2f4feea117ec9fb Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 08:02:27 -0500 Subject: [PATCH] Fix Codex tool-use transcript completion --- .../codex-local/src/ui/parse-stdout.test.ts | 83 +++++++++++++++++++ .../codex-local/src/ui/parse-stdout.ts | 54 ++++++++++-- ui/src/lib/issue-chat-messages.test.ts | 37 +++++++++ 3 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 packages/adapters/codex-local/src/ui/parse-stdout.test.ts diff --git a/packages/adapters/codex-local/src/ui/parse-stdout.test.ts b/packages/adapters/codex-local/src/ui/parse-stdout.test.ts new file mode 100644 index 00000000..976377fa --- /dev/null +++ b/packages/adapters/codex-local/src/ui/parse-stdout.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { parseCodexStdoutLine } from "./parse-stdout.js"; + +describe("parseCodexStdoutLine", () => { + it("marks completed tool_use items as resolved tool results", () => { + const started = parseCodexStdoutLine(JSON.stringify({ + type: "item.started", + item: { + id: "tool-1", + type: "tool_use", + name: "search", + input: { query: "paperclip" }, + }, + }), "2026-04-08T12:00:00.000Z"); + + const completed = parseCodexStdoutLine(JSON.stringify({ + type: "item.completed", + item: { + id: "tool-1", + type: "tool_use", + name: "search", + status: "completed", + }, + }), "2026-04-08T12:00:01.000Z"); + + expect(started).toEqual([{ + kind: "tool_call", + ts: "2026-04-08T12:00:00.000Z", + name: "search", + toolUseId: "tool-1", + input: { query: "paperclip" }, + }]); + expect(completed).toEqual([{ + kind: "tool_result", + ts: "2026-04-08T12:00:01.000Z", + toolUseId: "tool-1", + content: "search completed", + isError: false, + }]); + }); + + it("keeps explicit tool_result payloads authoritative after tool_use completion", () => { + const completed = parseCodexStdoutLine(JSON.stringify({ + type: "item.completed", + item: { + id: "tool-2", + type: "tool_result", + tool_use_id: "tool-1", + content: "final payload", + status: "completed", + }, + }), "2026-04-08T12:00:02.000Z"); + + expect(completed).toEqual([{ + kind: "tool_result", + ts: "2026-04-08T12:00:02.000Z", + toolUseId: "tool-1", + content: "final payload", + isError: false, + }]); + }); + + it("marks failed completed tool_use items as error results", () => { + const completed = parseCodexStdoutLine(JSON.stringify({ + type: "item.completed", + item: { + id: "tool-3", + type: "tool_use", + name: "write_file", + status: "error", + error: { message: "permission denied" }, + }, + }), "2026-04-08T12:00:03.000Z"); + + expect(completed).toEqual([{ + kind: "tool_result", + ts: "2026-04-08T12:00:03.000Z", + toolUseId: "tool-3", + content: "permission denied", + isError: true, + }]); + }); +}); diff --git a/packages/adapters/codex-local/src/ui/parse-stdout.ts b/packages/adapters/codex-local/src/ui/parse-stdout.ts index 0f1786b6..cb5661b9 100644 --- a/packages/adapters/codex-local/src/ui/parse-stdout.ts +++ b/packages/adapters/codex-local/src/ui/parse-stdout.ts @@ -118,6 +118,52 @@ function parseFileChangeItem(item: Record, ts: string): Transcr return [{ kind: "system", ts, text: `file changes: ${preview}${more}` }]; } +function parseToolUseItem( + item: Record, + ts: string, + phase: "started" | "completed", +): TranscriptEntry[] { + const name = asString(item.name, "unknown"); + const toolUseId = asString(item.id, name || "tool_use"); + + if (phase === "started") { + return [{ + kind: "tool_call", + ts, + name, + toolUseId, + input: item.input ?? {}, + }]; + } + + const status = asString(item.status); + const isError = + item.is_error === true || + status === "failed" || + status === "errored" || + status === "error" || + status === "cancelled"; + const rawContent = + item.content ?? + item.output ?? + item.result ?? + item.error ?? + item.message; + const content = + asString(rawContent) || + errorText(rawContent) || + stringifyUnknown(rawContent) || + `${name} ${isError ? "failed" : "completed"}`; + + return [{ + kind: "tool_result", + ts, + toolUseId, + content, + isError, + }]; +} + function parseCodexItem( item: Record, ts: string, @@ -146,13 +192,7 @@ function parseCodexItem( } if (itemType === "tool_use") { - return [{ - kind: "tool_call", - ts, - name: asString(item.name, "unknown"), - toolUseId: asString(item.id), - input: item.input ?? {}, - }]; + return parseToolUseItem(item, ts, phase); } if (itemType === "tool_result" && phase === "completed") { diff --git a/ui/src/lib/issue-chat-messages.test.ts b/ui/src/lib/issue-chat-messages.test.ts index f3ae87d7..388291f9 100644 --- a/ui/src/lib/issue-chat-messages.test.ts +++ b/ui/src/lib/issue-chat-messages.test.ts @@ -130,6 +130,43 @@ describe("buildAssistantPartsFromTranscript", () => { ]); }); + it("treats a completed tool-only segment as resolved once a tool_result arrives", () => { + const result = buildAssistantPartsFromTranscript([ + { kind: "thinking", ts: "2026-04-06T12:00:00.000Z", text: "Checking the task." }, + { + kind: "tool_call", + ts: "2026-04-06T12:00:01.000Z", + name: "search", + toolUseId: "tool-1", + input: { query: "paperclip" }, + }, + { + kind: "tool_result", + ts: "2026-04-06T12:00:02.000Z", + toolUseId: "tool-1", + content: "search completed", + isError: false, + }, + { kind: "assistant", ts: "2026-04-06T12:00:03.000Z", text: "Found the relevant code." }, + ]); + + expect(result.parts).toMatchObject([ + { type: "reasoning", text: "Checking the task." }, + { + type: "tool-call", + toolCallId: "tool-1", + toolName: "search", + result: "search completed", + isError: false, + }, + { type: "text", text: "Found the relevant code." }, + ]); + expect(result.segments).toEqual([{ + startMs: new Date("2026-04-06T12:00:00.000Z").getTime(), + endMs: new Date("2026-04-06T12:00:02.000Z").getTime(), + }]); + }); + it("keeps run errors while suppressing init and system transcript noise", () => { const result = buildAssistantPartsFromTranscript([ {