From 8ff63a6d715ade95f293205c8b6a54b58018349c Mon Sep 17 00:00:00 2001 From: xy-plus <40733434+xy-plus@users.noreply.github.com> Date: Tue, 19 May 2026 10:57:55 +0800 Subject: [PATCH] fix(server): keep Codex sub-agent running on transient child error state Fixes #1071 --- .../providers/codex-app-server-agent.test.ts | 34 +++++++++++ .../providers/codex/tool-call-mapper.test.ts | 58 +++++++++++++++++++ .../agent/providers/codex/tool-call-mapper.ts | 17 +++++- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 89ec1ccbe..8abafe00c 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -1352,6 +1352,40 @@ describe("Codex app-server provider", () => { }); }); + test("does not synthesize a parent sub-agent failure from child error state alone", () => { + const session = createSession(); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => events.push(event)); + + asInternals(session).handleNotification("item/completed", { + threadId: "test-thread", + item: { + type: "collabAgentToolCall", + id: "call-sub-agent-transient-child-error", + tool: "spawnAgent", + status: "completed", + prompt: "Validate the child agent result.", + receiverThreadIds: ["child-thread-1"], + agentsStates: { + "child-thread-1": { status: "error", message: "Sub-agent failed" }, + }, + }, + }); + + expect(events.at(-1)?.item).toMatchObject({ + type: "tool_call", + callId: "call-sub-agent-transient-child-error", + name: "Sub-agent", + status: "running", + error: null, + detail: { + type: "sub_agent", + subAgentType: "Sub-agent", + description: "Validate the child agent result.", + }, + }); + }); + test("loads Codex persisted history from the app-server thread", async () => { const session = createSession(); const requests: Array<{ method: string; params: unknown }> = []; diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts index ac14899fd..4271f4059 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts @@ -212,6 +212,64 @@ describe("codex tool-call mapper", () => { }); }); + it("does not fail a collabAgentToolCall from child error state alone", () => { + const item = mapCodexToolCallFromThreadItem({ + type: "collabAgentToolCall", + id: "call-sub-agent-transient-child-error", + tool: "spawnAgent", + status: "completed", + prompt: "Inspect the Codex stream path.", + receiverThreadIds: ["child-thread-1"], + agentsStates: { + "child-thread-1": { status: "error", message: "Sub-agent failed" }, + }, + }); + + expect(item).toEqual({ + type: "tool_call", + callId: "call-sub-agent-transient-child-error", + name: "Sub-agent", + status: "running", + error: null, + detail: { + type: "sub_agent", + subAgentType: "Sub-agent", + description: "Inspect the Codex stream path.", + log: "", + actions: [], + }, + }); + }); + + it("still fails a collabAgentToolCall from an explicitly failed child state", () => { + const item = mapCodexToolCallFromThreadItem({ + type: "collabAgentToolCall", + id: "call-sub-agent-child-failed", + tool: "spawnAgent", + status: "completed", + prompt: "Inspect the Codex stream path.", + receiverThreadIds: ["child-thread-1"], + agentsStates: { + "child-thread-1": { status: "failed", message: "Child failed" }, + }, + }); + + expect(item).toEqual({ + type: "tool_call", + callId: "call-sub-agent-child-failed", + name: "Sub-agent", + status: "failed", + error: { message: "Sub-agent failed" }, + detail: { + type: "sub_agent", + subAgentType: "Sub-agent", + description: "Inspect the Codex stream path.", + log: "", + actions: [], + }, + }); + }); + it("maps mcp read_file completion with detail", () => { const item = mapCodexToolCallFromThreadItem( { diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index 2c21288b7..cc6e0d4ca 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -544,6 +544,14 @@ function readStatus(value: unknown): string | undefined { return typeof value.status === "string" ? value.status : undefined; } +function normalizeCollabAgentChildStatus(status: string): ToolCallTimelineItem["status"] { + const normalized = status.trim().toLowerCase(); + if (normalized === "error" || normalized === "errored") { + return "running"; + } + return normalizeToolCallStatus(status, null, null); +} + function resolveCollabAgentStatus( item: z.infer, ): ToolCallTimelineItem["status"] { @@ -551,10 +559,15 @@ function resolveCollabAgentStatus( return "failed"; } + const parentStatus = normalizeToolCallStatus(item.status, null, null); + if (parentStatus === "failed") { + return "failed"; + } + const childStatuses = Object.values(item.agentsStates ?? {}) .map(readStatus) .filter((status): status is string => typeof status === "string" && status.trim().length > 0) - .map((status) => normalizeToolCallStatus(status, null, null)); + .map(normalizeCollabAgentChildStatus); if (childStatuses.some((status) => status === "failed")) { return "failed"; @@ -566,7 +579,7 @@ function resolveCollabAgentStatus( return childStatuses.every((status) => status === "completed") ? "completed" : "running"; } - return normalizeToolCallStatus(item.status, item.error ?? null, null); + return parentStatus; } function buildMcpToolName(server: string | undefined, tool: string): string {