From f24cd478c610dccd40f4e09643de3ac000e87e97 Mon Sep 17 00:00:00 2001 From: Emre K <110906681+kocaemre@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:51:43 +0200 Subject: [PATCH] fix: mark ripgrep no-match as completed Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com> --- src/CodexEventHandler.ts | 3 +- src/CodexToolCallMapper.ts | 3 +- src/CommandExecutionStatus.ts | 32 ++++++ .../command-execution-status.test.ts | 105 ++++++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 src/CommandExecutionStatus.ts create mode 100644 src/__tests__/CodexACPAgent/command-execution-status.test.ts diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 7b567541..9c58b50d 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -39,6 +39,7 @@ import type { WarningNotification } from "./app-server/v2"; import type { McpStartupCompleteEvent } from "./app-server/McpStartupCompleteEvent"; +import { commandExecutionAcpStatus } from "./CommandExecutionStatus"; import {toTokenCount} from "./TokenCount"; import { commandExecutionUsesTerminalOutput, @@ -1009,7 +1010,7 @@ export class CodexEventHandler { const update: UpdateSessionEvent = { sessionUpdate: "tool_call_update", toolCallId: item.id, - status: item.status === "completed" ? "completed" : "failed", + status: commandExecutionAcpStatus(item), rawOutput: { formatted_output: item.aggregatedOutput ?? "", exit_code: item.exitCode diff --git a/src/CodexToolCallMapper.ts b/src/CodexToolCallMapper.ts index d1b4cbb9..61f65fea 100644 --- a/src/CodexToolCallMapper.ts +++ b/src/CodexToolCallMapper.ts @@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import type { UpdateSessionEvent } from "./ACPSessionConnection"; import { stripShellPrefix } from "./CommandUtils"; +import { commandExecutionAcpStatus } from "./CommandExecutionStatus"; import type { FuzzyFileSearchSessionCompletedNotification, FuzzyFileSearchSessionUpdatedNotification @@ -110,7 +111,7 @@ export function createCommandExecutionCompleteUpdate( const update: UpdateSessionEvent = { sessionUpdate: "tool_call_update", toolCallId: item.id, - status: item.status === "completed" ? "completed" : "failed", + status: commandExecutionAcpStatus(item), rawOutput: { formatted_output: item.aggregatedOutput ?? "", exit_code: item.exitCode, diff --git a/src/CommandExecutionStatus.ts b/src/CommandExecutionStatus.ts new file mode 100644 index 00000000..5d4a02f9 --- /dev/null +++ b/src/CommandExecutionStatus.ts @@ -0,0 +1,32 @@ +import { stripShellPrefix } from "./CommandUtils"; +import type { ThreadItem } from "./app-server/v2"; + +type CommandExecutionItem = ThreadItem & { type: "commandExecution" }; + +export function commandExecutionAcpStatus(item: CommandExecutionItem): "completed" | "failed" { + if (item.status === "completed") { + return "completed"; + } + + if (isRipgrepNoMatch(item)) { + return "completed"; + } + + return "failed"; +} + +function isRipgrepNoMatch(item: CommandExecutionItem): boolean { + return item.status === "failed" + && item.exitCode === 1 + && !item.aggregatedOutput + && item.commandActions.length === 0 + && isStandaloneRipgrepCommand(stripShellPrefix(item.command).trim()); +} + +function isStandaloneRipgrepCommand(command: string): boolean { + if (!/^(?:rg|ripgrep)(?:\s|$)/.test(command)) { + return false; + } + + return !/[;&|`$<>\n\r]/.test(command); +} diff --git a/src/__tests__/CodexACPAgent/command-execution-status.test.ts b/src/__tests__/CodexACPAgent/command-execution-status.test.ts new file mode 100644 index 00000000..7c2d3e55 --- /dev/null +++ b/src/__tests__/CodexACPAgent/command-execution-status.test.ts @@ -0,0 +1,105 @@ +import {describe, expect, it, vi} from "vitest"; +import type {ServerNotification} from "../../app-server"; +import type {ThreadItem} from "../../app-server/v2"; +import {createCommandExecutionCompleteUpdate} from "../../CodexToolCallMapper"; +import { + createCodexMockTestFixture, + createTestSessionState, + setupPromptAndSendNotifications, +} from "../acp-test-utils"; + +type CommandExecutionItem = Extract; + +describe("command execution status mapping", () => { + it("maps a standalone ripgrep no-match exit as completed in replay", () => { + expect(createCommandExecutionCompleteUpdate(command({ + command: 'rg -n "__definitely_absent_token__" README.md', + status: "failed", + exitCode: 1, + aggregatedOutput: "", + }), "terminal_output_delta")).toMatchObject({ + sessionUpdate: "tool_call_update", + toolCallId: "command-1", + status: "completed", + rawOutput: { + formatted_output: "", + exit_code: 1, + }, + }); + }); + + it("maps a standalone ripgrep no-match exit as completed in live events", async () => { + const fixture = createCodexMockTestFixture(); + const sessionId = "thread-1"; + const sessionState = createTestSessionState({sessionId}); + + await setupPromptAndSendNotifications(fixture, sessionId, sessionState, [ + completed(command({ + command: "/bin/zsh -lc 'rg -n \"__definitely_absent_token__\" README.md'", + status: "failed", + exitCode: 1, + aggregatedOutput: "", + }), sessionId), + ]); + + expect(fixture.getAcpConnectionEvents([]) + .filter(event => event.method === "sessionUpdate") + .map(event => event.args[0].update) + ).toContainEqual(expect.objectContaining({ + sessionUpdate: "tool_call_update", + toolCallId: "command-1", + status: "completed", + rawOutput: { + formatted_output: "", + exit_code: 1, + }, + })); + }); + + it("keeps ripgrep diagnostics and compound shell failures failed", () => { + expect(createCommandExecutionCompleteUpdate(command({ + command: "rg -n '[' README.md", + status: "failed", + exitCode: 2, + aggregatedOutput: "regex parse error", + }), "terminal_output_delta")).toMatchObject({status: "failed"}); + + expect(createCommandExecutionCompleteUpdate(command({ + command: "printf ok && rg -n absent README.md", + status: "failed", + exitCode: 1, + aggregatedOutput: "ok", + }), "terminal_output_delta")).toMatchObject({status: "failed"}); + }); +}); + +function command(overrides: Partial = {}): CommandExecutionItem { + return { + type: "commandExecution", + id: "command-1", + pluginId: null, + scriptPath: null, + command: "rg -n absent README.md", + cwd: "/workspace", + processId: "42", + source: "unifiedExecStartup", + status: "inProgress", + commandActions: [], + aggregatedOutput: null, + exitCode: null, + durationMs: null, + ...overrides, + }; +} + +function completed(item: ThreadItem, threadId: string): ServerNotification { + return { + method: "item/completed", + params: { + threadId, + turnId: "turn-id", + completedAtMs: 0, + item, + }, + }; +}