Skip to content

Commit 37c31a2

Browse files
committed
fix(codex): normalize native command exit status
1 parent b13fb78 commit 37c31a2

2 files changed

Lines changed: 57 additions & 6 deletions

File tree

extensions/codex/src/app-server/event-projector.test.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,13 +1695,49 @@ describe("CodexAppServerEventProjector", () => {
16951695
expect(toolResult.result).toEqual({ status: "completed", exitCode: 0, durationMs: 42 });
16961696
});
16971697

1698+
it("treats nonzero native command exits as completed tool results", async () => {
1699+
const onAgentEvent = vi.fn();
1700+
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
1701+
1702+
await projector.handleNotification(
1703+
turnCompleted([
1704+
{
1705+
type: "commandExecution",
1706+
id: "cmd-no-matches",
1707+
command: "grep -R missing docs",
1708+
cwd: "/workspace",
1709+
processId: null,
1710+
source: "agent",
1711+
status: "failed",
1712+
commandActions: [],
1713+
aggregatedOutput: "",
1714+
exitCode: 1,
1715+
durationMs: 42,
1716+
},
1717+
]),
1718+
);
1719+
1720+
expect(projector.buildResult(buildEmptyToolTelemetry()).lastToolError).toBeUndefined();
1721+
const toolResult = findAgentEvent(onAgentEvent, {
1722+
stream: "tool",
1723+
phase: "result",
1724+
itemId: "cmd-no-matches",
1725+
name: "bash",
1726+
}).data;
1727+
expect(toolResult).toMatchObject({
1728+
status: "completed",
1729+
isError: false,
1730+
result: { status: "completed", exitCode: 1, durationMs: 42 },
1731+
});
1732+
});
1733+
16981734
it("uses streamed command output for failed native tool errors", async () => {
16991735
const projector = await createProjector();
17001736

17011737
await projector.handleNotification(
17021738
forCurrentTurn("item/commandExecution/outputDelta", {
17031739
itemId: "cmd-streamed-failure",
1704-
delta: "fatal: missing fixture\n",
1740+
delta: "pnpm: command not found\n",
17051741
}),
17061742
);
17071743
await projector.handleNotification(
@@ -1716,7 +1752,7 @@ describe("CodexAppServerEventProjector", () => {
17161752
status: "failed",
17171753
commandActions: [],
17181754
aggregatedOutput: null,
1719-
exitCode: 1,
1755+
exitCode: 127,
17201756
durationMs: 42,
17211757
},
17221758
]),
@@ -1725,7 +1761,7 @@ describe("CodexAppServerEventProjector", () => {
17251761
expect(projector.buildResult(buildEmptyToolTelemetry()).lastToolError).toEqual({
17261762
toolName: "bash",
17271763
meta: "run tests (workspace)",
1728-
error: "fatal: missing fixture",
1764+
error: "pnpm: command not found",
17291765
mutatingAction: true,
17301766
actionFingerprint: JSON.stringify({
17311767
type: "commandExecution",

extensions/codex/src/app-server/event-projector.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ const MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM = 20;
110110
const TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS = 12_000;
111111
const GENERATED_IMAGE_MEDIA_SUBDIR = "tool-image-generation";
112112
const BYTES_PER_MB = 1024 * 1024;
113+
const SHELL_INVOCATION_FAILURE_EXIT_CODES = new Set([126, 127]);
113114
// Match OpenClaw's default image media cap for generated image tool outputs.
114115
const DEFAULT_GENERATED_IMAGE_MAX_BYTES = 6 * BYTES_PER_MB;
115116
const TRANSCRIPT_PROGRESS_SUPPRESSED_TOOL_NAMES = new Set([
@@ -1924,6 +1925,9 @@ function itemTitle(item: CodexThreadItem): string {
19241925
function itemStatus(item: CodexThreadItem): "completed" | "failed" | "running" | "blocked" {
19251926
const status = readItemString(item, "status");
19261927
if (status === "failed") {
1928+
if (isCompletedCommandExit(item)) {
1929+
return "completed";
1930+
}
19271931
return "failed";
19281932
}
19291933
if (status === "declined") {
@@ -1939,6 +1943,17 @@ function isNonSuccessItemStatus(status: ReturnType<typeof itemStatus>): boolean
19391943
return status === "failed" || status === "blocked";
19401944
}
19411945

1946+
function isCompletedCommandExit(item: CodexThreadItem): boolean {
1947+
// Codex marks nonzero native shell exits as failed. OpenClaw's exec contract
1948+
// treats process exits as completed tool runs; callers read exitCode/output
1949+
// for command semantics, while 126/127 still mean the shell could not run it.
1950+
return (
1951+
item.type === "commandExecution" &&
1952+
typeof item.exitCode === "number" &&
1953+
!SHELL_INVOCATION_FAILURE_EXIT_CODES.has(item.exitCode)
1954+
);
1955+
}
1956+
19421957
function itemName(item: CodexThreadItem): string | undefined {
19431958
if (item.type === "dynamicToolCall" && typeof item.tool === "string") {
19441959
return item.tool;
@@ -2082,7 +2097,7 @@ function itemToolResult(item: CodexThreadItem): { result?: Record<string, unknow
20822097
if (item.type === "commandExecution") {
20832098
return {
20842099
result: sanitizeCodexAgentEventRecord({
2085-
status: item.status,
2100+
status: itemStatus(item),
20862101
exitCode: item.exitCode,
20872102
durationMs: item.durationMs,
20882103
}),
@@ -2091,15 +2106,15 @@ function itemToolResult(item: CodexThreadItem): { result?: Record<string, unknow
20912106
if (item.type === "fileChange") {
20922107
return {
20932108
result: sanitizeCodexAgentEventRecord({
2094-
status: item.status,
2109+
status: itemStatus(item),
20952110
changes: itemFileChanges(item),
20962111
}),
20972112
};
20982113
}
20992114
if (item.type === "mcpToolCall") {
21002115
return {
21012116
result: sanitizeCodexAgentEventRecord({
2102-
status: item.status,
2117+
status: itemStatus(item),
21032118
durationMs: item.durationMs,
21042119
...(item.error ? { error: item.error } : {}),
21052120
...(item.result ? { result: item.result } : {}),

0 commit comments

Comments
 (0)