From 02216d601c1070bd7d619b3a40b1b021af37cb66 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:20:11 +0200 Subject: [PATCH] fix(pi): settle foreground workflow tools when the process exits Deliver a failed terminal mapping for calls still owned by the Pi process. Detached runs have already returned their tool result, so their adapter-owned status is preserved. --- .../server/agent/providers/pi/agent.test.ts | 46 +++++++++++++++++++ .../src/server/agent/providers/pi/agent.ts | 9 ++++ 2 files changed, 55 insertions(+) diff --git a/packages/server/src/server/agent/providers/pi/agent.test.ts b/packages/server/src/server/agent/providers/pi/agent.test.ts index 927a9defd2..37e43ca36f 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -2101,6 +2101,52 @@ describe("PiRpcAgentSession", () => { }); }); + test("fails an unfinished workflow on process exit while preserving detached runs", async () => { + const { pi, session, events } = await createSession(); + onTestFinished(() => session.close()); + const updates: AgentStreamEvent[] = []; + session.subscribe((event) => updates.push(event)); + await session.startTurn("run workflows"); + const runtime = pi.latestSession(); + runtime.emit({ + type: "tool_execution_start", + toolCallId: "detached", + toolName: "subagent", + args: { workflowScript: "return await runs.all([]);", async: true }, + }); + runtime.emit({ + type: "tool_execution_end", + toolCallId: "detached", + toolName: "subagent", + result: { + details: { + mode: "workflow", + runId: "detached-run", + asyncId: "detached-run", + asyncDir: "/nonexistent/pi-exit-regression", + results: [], + }, + }, + }); + runtime.emit({ + type: "tool_execution_start", + toolCallId: "foreground", + toolName: "subagent", + args: { workflowScript: "return await runs.all([]);", async: false }, + }); + runtime.emit({ type: "process_exit", error: "Pi exited" }); + + await expect(events.nextTurnFailure()).resolves.toMatchObject({ error: "Pi exited" }); + const statuses = (id: string) => + updates.flatMap((event) => + event.type === "provider_subagent" && event.event.type === "upsert" && event.event.id === id + ? [event.event.status] + : [], + ); + expect(statuses("foreground")).toEqual(["running", "failed"]); + expect(statuses("detached")).toEqual(["running", "running"]); + }); + test("fails an autonomous turn when the Pi process exits before settlement", async () => { const { pi, events } = await createSession(); const fakeSession = pi.latestSession(); diff --git a/packages/server/src/server/agent/providers/pi/agent.ts b/packages/server/src/server/agent/providers/pi/agent.ts index 146b56b36f..622c616125 100644 --- a/packages/server/src/server/agent/providers/pi/agent.ts +++ b/packages/server/src/server/agent/providers/pi/agent.ts @@ -2108,6 +2108,15 @@ export class PiRpcAgentSession implements AgentSession { } private handleProcessExit(error: string): void { + for (const [toolCallId, toolCall] of this.activeToolCalls) { + this.handleToolExecutionEnd({ + type: "tool_execution_end", + toolCallId, + toolName: toolCall.toolName, + isError: true, + result: { content: [{ type: "text", text: error }] }, + }); + } this.rejectAllExtensionResults(new Error(error)); this.interruptingTurn = null; if (!this.activeTurnId && !this.activeTurnStarted) {