From b1d1284cc891d40f1ce5a331a796a99b09c46d15 Mon Sep 17 00:00:00 2001 From: User Date: Tue, 14 Jul 2026 20:44:59 +0200 Subject: [PATCH] fix(backend): restart idle Pi sessions on resume --- backend/src/routes/sessions.ts | 12 ++++--- backend/test/routes-sessions.test.ts | 50 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index 138272e9..ab0c7295 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -100,10 +100,14 @@ export function sessionRoutes( }); app.post("/sessions/:id/resume", async (req, reply) => { const id = (req.params as any).id; - if (d.mgr.get(id)) { - // Idempotent resume: reattach the browser to the existing Pi runtime. - // Do not respawn it (which would discard a pending reviewer widget). - return reply.code(200).send({ id, alreadyActive: true }); + const existing = d.mgr.get(id); + if (existing) { + const state = existing.bridge.turnState(); + if (state === "running" || state === "waiting") { + return reply.code(200).send({ id, alreadyActive: true }); + } + d.mgr.teardown(id); + d.hub.clear(id); } const manifest = (await d.tht.sessionShow(id, d.getSettings().workspace)) as { status?: string; archived?: boolean } | null; if (manifest?.status === "finalized" || manifest?.archived) { diff --git a/backend/test/routes-sessions.test.ts b/backend/test/routes-sessions.test.ts index 07f90b36..9dd9bf4e 100644 --- a/backend/test/routes-sessions.test.ts +++ b/backend/test/routes-sessions.test.ts @@ -133,6 +133,56 @@ test("POST /sessions/:id/resume usa il thinking globale se manca nel manifest", expect(configured.thinking).toBe("low"); }); +test.each(["running", "waiting"])( + "POST resume preserves a %s runtime", + async (state) => { + let tornDown = false; + const existing = { bridge: { turnState: () => state } } as any; + const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), { + mgr: { + get: () => existing, + teardown: () => { tornDown = true; }, + } as any, + thtRunner: {} as any, + getSettings: () => ({ workspace: "psd" }) as any, + }); + const response = await app.inject({ method: "POST", url: "/sessions/s1/resume" }); + expect(response.json()).toEqual({ id: "s1", alreadyActive: true }); + expect(tornDown).toBe(false); + }, +); + +test.each(["idle", "failed"])( + "POST resume replaces a %s runtime and starts the persisted session", + async (state) => { + const order: string[] = []; + const oldRuntime = { bridge: { turnState: () => state } } as any; + const newRuntime = { bridge: { onClientEvent: () => {}, emitClientEvent: () => {} } } as any; + const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), { + mgr: { + get: () => oldRuntime, + teardown: (id: string) => order.push(`teardown:${id}`), + createFor: () => { order.push("create"); return newRuntime; }, + configure: async () => {}, + start: () => order.push("start"), + } as any, + thtRunner: { + sessionShow: async () => ({ + status: "open", archived: false, + provider: "local-qwen", model: "qwen3.6-35b-a3b", thinking: "low", + }), + reopenSession: async () => order.push("reopen"), + } as any, + readiness: { ensure: async () => ({ ok: true }) } as any, + getSettings: () => ({ workspace: "local", thinking: "medium" }) as any, + }); + const response = await app.inject({ method: "POST", url: "/sessions/s1/resume" }); + await new Promise((resolve) => setImmediate(resolve)); + expect(response.json()).toEqual({ id: "s1" }); + expect(order).toEqual(["teardown:s1", "reopen", "create", "start"]); + }, +); + test("POST /sessions/:id/response inoltra al bridge (no error)", async () => { const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), { thtRunner: {