From a791919925787d988727ed4f007ece6ac5697a8a Mon Sep 17 00:00:00 2001 From: mptyl Date: Mon, 29 Jun 2026 20:06:03 +0200 Subject: [PATCH] fix(backend): check read-only 409 before the Ollama preflight on resume --- backend/src/routes/sessions.ts | 4 ++-- backend/test/routes-sessions.test.ts | 11 +++++++++++ .../specs/2026-06-29-ollama-ensure-design.md | 7 ++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index 4c21c47d..cfc29212 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -50,12 +50,12 @@ export function sessionRoutes( }); app.post("/sessions/:id/resume", async (req, reply) => { const id = (req.params as any).id; - const ensure = await d.tht.ollamaEnsure(d.getSettings().workspace ?? "", d.ollamaEnsureTimeoutSec); - if (!ensure.ok) return reply.code(503).send({ error: ensure.error ?? "Ollama/embeddings non disponibili" }); const manifest = (await d.tht.sessionShow(id)) as { status?: string; archived?: boolean } | null; if (manifest?.status === "finalized" || manifest?.archived) { return reply.code(409).send({ error: "sessione in sola lettura (finalizzata o archiviata)" }); } + const ensure = await d.tht.ollamaEnsure(d.getSettings().workspace ?? "", d.ollamaEnsureTimeoutSec); + if (!ensure.ok) return reply.code(503).send({ error: ensure.error ?? "Ollama/embeddings non disponibili" }); const rt = await d.mgr.resume(id, d.tht); rt.bridge.onClientEvent((e) => d.hub.publish(id, e.type, e)); return reply.code(200).send({ id }); diff --git a/backend/test/routes-sessions.test.ts b/backend/test/routes-sessions.test.ts index 9162892e..e54bc278 100644 --- a/backend/test/routes-sessions.test.ts +++ b/backend/test/routes-sessions.test.ts @@ -152,6 +152,17 @@ test("POST /sessions proceeds when ollamaEnsure succeeds", async () => { expect(ensureWs).toBe("psd"); }); +test("POST /sessions/:id/resume returns 409 for a read-only session without calling ollamaEnsure", async () => { + let ensureCalled = false; + const app = mutApp({ + sessionShow: async () => ({ status: "finalized", archived: false }), + ollamaEnsure: async () => { ensureCalled = true; return { ok: false, error: "down" }; }, + }); + const res = await app.inject({ method: "POST", url: "/sessions/s1/resume" }); + expect(res.statusCode).toBe(409); + expect(ensureCalled).toBe(false); +}); + test("POST /sessions/:id/resume refuses with 503 when ollamaEnsure fails", async () => { const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), { thtRunner: { diff --git a/docs/superpowers/specs/2026-06-29-ollama-ensure-design.md b/docs/superpowers/specs/2026-06-29-ollama-ensure-design.md index b3da6e86..3b762486 100644 --- a/docs/superpowers/specs/2026-06-29-ollama-ensure-design.md +++ b/docs/superpowers/specs/2026-06-29-ollama-ensure-design.md @@ -103,9 +103,10 @@ and the start spawn behind a seam. The preflight runs **first**, before any session is created or resumed: - `POST /sessions`: `const r = await tht.ollamaEnsure(settings.workspace, timeout)`. If `!r.ok` → `reply.code(503).send({ error: r.error })` and **return** (do NOT call `sessionNew`/`spawnFor`). -- `POST /sessions/:id/resume`: same preflight **before** the existing finalized/archived guard - and `mgr.resume`. On failure → 503, no spawn. (Uses the session's workspace — the current - configured/settings workspace, consistent with how resume resolves the session.) +- `POST /sessions/:id/resume`: the finalized/archived guard runs **first** — a read-only session + is rejected with 409 without touching Ollama. The preflight runs **after** that guard and + **before** `mgr.resume`. On failure → 503, no spawn. (Uses the session's workspace — the + current configured/settings workspace, consistent with how resume resolves the session.) ### Config ([config.ts](../../../backend/src/config.ts)) - `ollamaEnsureTimeoutMs: number` from `OLLAMA_ENSURE_TIMEOUT_MS` (default `60000`); passed to