From 0e6108a1a5819103a29631c54035b1cd482fe4c1 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 23 Jul 2026 18:52:19 -0700 Subject: [PATCH] fix(dashboard): Stop cancels pending initial planning turns per-session stopGeneration only aborted generations with an activeGenerations record. A just-started session whose initial turn was still pending (registered by start-streaming, not yet consumed by a stream connect) returned false from Stop and the "stopped" generation sprang back to life on the next stream connect. Stop now discards the pending turn too, and remains strictly keyed to its session id so stopping one plan never affects other concurrently generating sessions. Co-Authored-By: Claude Fable 5 --- .../planning-stop-pending-initial-turn.md | 7 +++ .../planning-generation-cancellation.test.ts | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .changeset/planning-stop-pending-initial-turn.md diff --git a/.changeset/planning-stop-pending-initial-turn.md b/.changeset/planning-stop-pending-initial-turn.md new file mode 100644 index 0000000000..93455ac90c --- /dev/null +++ b/.changeset/planning-stop-pending-initial-turn.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Stopping a plan now also cancels generations that haven't started streaming yet. +category: fix +dev: `stopGeneration` discards a still-pending initial turn (registered by start-streaming but not yet consumed by a stream connect) instead of returning false and letting the "stopped" generation restart on the next connect; stops remain strictly per-session when multiple plans generate concurrently. diff --git a/packages/dashboard/src/__tests__/planning-generation-cancellation.test.ts b/packages/dashboard/src/__tests__/planning-generation-cancellation.test.ts index 2a3315a593..aacccc44da 100644 --- a/packages/dashboard/src/__tests__/planning-generation-cancellation.test.ts +++ b/packages/dashboard/src/__tests__/planning-generation-cancellation.test.ts @@ -103,4 +103,47 @@ describe("planning generation cancellation", () => { await new Promise((resolve) => setTimeout(resolve, 0)); expect(promptResolvedAfterAbort).toBe(true); }); + + /* + FNXC:PlanningStopMultiSession 2026-07-23-23:50: + Stop must cancel a session whose initial turn is still PENDING (start-streaming registered + it, but no stream connect consumed it yet) and must never touch other sessions' generations. + Previously stop returned false here and the "stopped" turn sprang back to life on the next + stream connect. + */ + it("cancels a pending initial turn and leaves other sessions' generations untouched", async () => { + const prompt = vi.fn(async () => {}); + __setCreateFnAgent(vi.fn(async () => ({ + session: { state: { messages: [] }, prompt, dispose: vi.fn() }, + })) as any); + + const stoppedSessionId = await createSessionWithAgent( + "10.0.2.11", + "Plan that gets stopped before its stream connects", + "/tmp/project", + MOCK_TASK_STORE, + ); + const survivorSessionId = await createSessionWithAgent( + "10.0.2.12", + "Plan that keeps generating", + "/tmp/project", + MOCK_TASK_STORE, + ); + + expect(planningStreamManager.hasPendingInitialTurn(stoppedSessionId)).toBe(true); + expect(stopGeneration(stoppedSessionId)).toBe(true); + + // The discarded turn must not fire on a later stream connect. + expect(planningStreamManager.hasPendingInitialTurn(stoppedSessionId)).toBe(false); + expect(planningStreamManager.consumeInitialTurn(stoppedSessionId)).toBeUndefined(); + expect(prompt).not.toHaveBeenCalled(); + + // The other session's pending turn is untouched and still runs normally. + expect(planningStreamManager.hasPendingInitialTurn(survivorSessionId)).toBe(true); + planningStreamManager.consumeInitialTurn(survivorSessionId)?.(); + for (let i = 0; i < 20 && prompt.mock.calls.length === 0; i++) { + await new Promise((resolve) => setTimeout(resolve, 5)); + } + expect(prompt).toHaveBeenCalled(); + }); });