From ce979a7e018ceac1c0bb18a2ceb1c874be43bda4 Mon Sep 17 00:00:00 2001 From: Exotic209093 <134711311+Exotic209093@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:57:27 +0100 Subject: [PATCH 1/2] fix(client-runtime): map cancelled and interrupted subagent terminal statuses Codex subagents that complete with 'cancelled' or 'interrupted' status were falling through to 'running' because TASK_COMPLETED_STATUS only covered completed, failed, and stopped. Add both missing entries so the fold correctly marks them as terminal. Fixes #11164 --- .../src/state/subagentRuntime.test.ts | 32 +++++++++++++++++++ .../src/state/subagentRuntime.ts | 2 ++ 2 files changed, 34 insertions(+) diff --git a/packages/client-runtime/src/state/subagentRuntime.test.ts b/packages/client-runtime/src/state/subagentRuntime.test.ts index d366d7f0d4ee..cf65cc4385fa 100644 --- a/packages/client-runtime/src/state/subagentRuntime.test.ts +++ b/packages/client-runtime/src/state/subagentRuntime.test.ts @@ -181,6 +181,38 @@ describe("foldSubagentActivities", () => { expect(agents[0]!.completedAt).toBe("2026-08-01T11:00:00.000Z"); }); + it("maps cancelled status to a terminal cancelled state", () => { + const agents = fold([ + activity("task.started", { taskId: "task-cancelled", taskType: "local_agent" }), + activity("task.completed", { + taskId: "task-cancelled", + status: "cancelled", + summary: "user cancelled", + }), + ]); + expect(agents).toHaveLength(1); + const agent = agents[0]!; + expect(agent.status).toBe("cancelled"); + expect(agent.result).toBe("user cancelled"); + expect(agent.completedAt).not.toBeNull(); + }); + + it("maps interrupted status to a terminal interrupted state", () => { + const agents = fold([ + activity("task.started", { taskId: "task-interrupted", taskType: "local_agent" }), + activity("task.completed", { + taskId: "task-interrupted", + status: "interrupted", + summary: "context window exceeded", + }), + ]); + expect(agents).toHaveLength(1); + const agent = agents[0]!; + expect(agent.status).toBe("interrupted"); + expect(agent.result).toBe("context window exceeded"); + expect(agent.completedAt).not.toBeNull(); + }); + it("reactivation increments the run count and clears result/error", () => { const agents = fold([ activity("task.started", { taskId: "task-4", taskType: "local_agent" }), diff --git a/packages/client-runtime/src/state/subagentRuntime.ts b/packages/client-runtime/src/state/subagentRuntime.ts index e441de32db48..962d74d003ca 100644 --- a/packages/client-runtime/src/state/subagentRuntime.ts +++ b/packages/client-runtime/src/state/subagentRuntime.ts @@ -430,6 +430,8 @@ const TASK_COMPLETED_STATUS: ReadonlyMap = new Ma ["completed", "completed"], ["failed", "failed"], ["stopped", "interrupted"], + ["cancelled", "cancelled"], + ["interrupted", "interrupted"], ]); const KNOWN_STATUSES: ReadonlySet = new Set([ From 177bd5d960f3a3bbff55d8d78b75b86784d79dfd Mon Sep 17 00:00:00 2001 From: James Date: Thu, 17 Sep 2026 22:29:28 +0100 Subject: [PATCH 2/2] fix(contracts): allow cancelled and interrupted task.completed statuses The client-runtime mapping for cancelled/interrupted subagent terminal statuses was unreachable live: TaskCompletedPayload still limited its status field to completed|failed|stopped, while the Claude adapter forwards the SDK message status straight into that payload. Extend the literals so the live wire contract carries the new statuses. Co-Authored-By: Claude Fable 5.1 --- packages/contracts/src/providerRuntime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/providerRuntime.ts b/packages/contracts/src/providerRuntime.ts index 01a8f9ad8f83..773fb6641da2 100644 --- a/packages/contracts/src/providerRuntime.ts +++ b/packages/contracts/src/providerRuntime.ts @@ -670,7 +670,7 @@ export type TaskUpdatedPayload = typeof TaskUpdatedPayload.Type; const TaskCompletedPayload = Schema.Struct({ taskId: RuntimeTaskId, - status: Schema.Literals(["completed", "failed", "stopped"]), + status: Schema.Literals(["completed", "failed", "stopped", "cancelled", "interrupted"]), summary: Schema.optional(TrimmedNonEmptyStringSchema), usage: Schema.optional(Schema.Unknown), typedUsage: Schema.optional(RuntimeTaskUsage),