From 151b705b47ae6f2336a09be023cd1798b1c5ca07 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:48:18 +0000 Subject: [PATCH] fix(pi): preserve recovery before prompt acknowledgement Own intentional-close lifecycle reporting at Pi event publication so pending RPC rejections and process exits cannot consume unfinished work. Preserve cleanup output and errors from open sessions. --- .../server/agent/providers/pi/agent.test.ts | 23 ++++++++++++++++ .../src/server/agent/providers/pi/agent.ts | 27 ++++++++++++------- .../agent/providers/pi/test-utils/fake-pi.ts | 3 +++ 3 files changed, 43 insertions(+), 10 deletions(-) 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 c04683bec2..0997e55cdd 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -2202,6 +2202,29 @@ describe("PiRpcAgentSession", () => { }, ); + test("closing Pi before prompt acknowledgement preserves the unfinished turn", async () => { + const { pi, session, events } = await createSession(); + pi.latestSession().holdNextPrompt(); + await session.startTurn("keep working"); + expect(pi.latestSession().prompts).toHaveLength(1); + const before = events.turnLifecycleEvents(); + + await session.close(); + + expect(events.turnLifecycleEvents()).toEqual(before); + }); + + test("a rejected prompt still fails an open Pi session", async () => { + const { pi, session, events } = await createSession(); + pi.latestSession().holdNextPrompt(); + await session.startTurn("keep working"); + + await pi.latestSession().failHeldPrompt(new Error("Prompt rejected")); + + await expect(events.nextTurnFailure()).resolves.toMatchObject({ error: "Prompt rejected" }); + await session.close(); + }); + test("fails the active turn when the Pi process exits mid-turn", async () => { const { pi, session, events } = await createSession(); diff --git a/packages/server/src/server/agent/providers/pi/agent.ts b/packages/server/src/server/agent/providers/pi/agent.ts index 89276b199f..2360d7dbf3 100644 --- a/packages/server/src/server/agent/providers/pi/agent.ts +++ b/packages/server/src/server/agent/providers/pi/agent.ts @@ -1729,6 +1729,17 @@ export class PiRpcAgentSession implements AgentSession { } private publish(event: AgentStreamEvent): void { + // Runtime release preserves unfinished work for restart recovery. Cleanup + // may still emit timeline updates, but must not settle or restart its turn. + if ( + this.closed && + (event.type === "turn_started" || + event.type === "turn_completed" || + event.type === "turn_canceled" || + event.type === "turn_failed") + ) { + return; + } for (const subscriber of this.subscribers) subscriber(event); } @@ -2204,16 +2215,12 @@ export class PiRpcAgentSession implements AgentSession { this.pendingSettledMessages = null; this.pendingSteerSubmissions.length = 0; this.clearNoTurnBuffers(); - // Releasing the runtime is not a failed turn: restart recovery still owns - // unfinished work after the session has been intentionally closed. - if (!this.closed) { - this.emit({ - type: "turn_failed", - provider: this.provider, - turnId, - error, - }); - } + this.emit({ + type: "turn_failed", + provider: this.provider, + turnId, + error, + }); } private handleSessionEvent(event: PiAgentSessionEvent): void { diff --git a/packages/server/src/server/agent/providers/pi/test-utils/fake-pi.ts b/packages/server/src/server/agent/providers/pi/test-utils/fake-pi.ts index b375b291a1..bc5bd00c89 100644 --- a/packages/server/src/server/agent/providers/pi/test-utils/fake-pi.ts +++ b/packages/server/src/server/agent/providers/pi/test-utils/fake-pi.ts @@ -427,6 +427,9 @@ export class FakePiSession implements PiRuntimeSession { async close(): Promise { this.closeCalls += 1; + if (this.activeHeldPrompt) { + await this.failHeldPrompt(new Error("Pi RPC session is closed")); + } this.emit({ type: "process_exit", error: "Pi RPC process exited with code 0 and signal null",