From ed033e71d599f383016e51c000109de74fe5b560 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 9 Sep 2026 17:34:56 +0100 Subject: [PATCH] fix(confidence-engine): sanitize focused reasoning outage --- .../deconstruct/route.js | 8 ++++- docs/current-handoff.md | 6 ++++ docs/current-project-state.md | 6 ++++ lib/llm/provider.js | 3 ++ tests/focused-deconstruct-boundary.test.js | 34 +++++++++++++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/app/api/focused-investigation/deconstruct/route.js b/app/api/focused-investigation/deconstruct/route.js index 8a948af..807cb6c 100644 --- a/app/api/focused-investigation/deconstruct/route.js +++ b/app/api/focused-investigation/deconstruct/route.js @@ -144,9 +144,15 @@ async function post(request) { }); console.info("[api/focused-investigation/deconstruct] end", { targetNodeId, - status: 500, + status: e?.code === "PROVIDER_UNAVAILABLE" ? 503 : 500, elapsedMs, }); + if (e?.code === "PROVIDER_UNAVAILABLE") { + return Response.json( + { error: "Reasoning service is temporarily unavailable." }, + { status: 503 }, + ); + } return Response.json( { error: e.message || "Unknown server error" }, { status: 500 }, diff --git a/docs/current-handoff.md b/docs/current-handoff.md index 41779b1..bd0e124 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -3,6 +3,12 @@ > **Role:** Concise operational snapshot for resuming work today. Not a historical diary. > The design evolution archive index at `docs/design-evolution/README.md` provides progressive loading of experiment history; load the relevant chapter only when a specific historical question requires it. +## Focused-investigation provider outage boundary + +- Focused-investigation outage handling now sanitizes provider failure at the API boundary: unavailable focused reasoning returns a controlled HTTP 503, and raw provider/Ollama diagnostics no longer leave that boundary. +- The existing generic Retry UX remains unchanged. A live deployed outage had already proven investigation preservation. +- `/api/cases/start` and `/api/cases/update` outage sanitization remain separately unverified; this change does not claim those routes are fixed. + ## v0.62d production Docker packaging — LIVE PROVEN - `Dockerfile` — minimal multi-stage Alpine build (Node 22), Next.js standalone output mode diff --git a/docs/current-project-state.md b/docs/current-project-state.md index 82bede0..557aa5a 100644 --- a/docs/current-project-state.md +++ b/docs/current-project-state.md @@ -1,5 +1,11 @@ # Current Project State — Confidence Engine +## Focused-Investigation Provider Outage Boundary + +- Focused-investigation outage handling now sanitizes provider failure at the API boundary: unavailable focused reasoning returns a controlled HTTP 503, and raw provider/Ollama diagnostics no longer leave that boundary. +- The existing generic Retry UX remains unchanged. A live deployed outage had already proven investigation preservation. +- `/api/cases/start` and `/api/cases/update` outage sanitization remain separately unverified; this change does not claim those routes are fixed. + ## v0.62d Production Docker Packaging — LIVE PROVEN - `Dockerfile` created: multi-stage Alpine build (Node 22), Next.js standalone output, configurable port 3000, health-check boundary via `/api/health` diff --git a/lib/llm/provider.js b/lib/llm/provider.js index ef3987d..d8d26e1 100644 --- a/lib/llm/provider.js +++ b/lib/llm/provider.js @@ -439,6 +439,9 @@ class OllamaLlmProvider { `- Use a smaller model (e.g., llama3.1 instead of llama3.1:70b)\n` + `- Check Ollama logs: \`ollama serve\` or look at your system logs` ); + if (e?.name === "AbortError" || e instanceof TypeError) { + error.code = "PROVIDER_UNAVAILABLE"; + } error.providerApiPath = apiUsed; error.providerExecution = providerExecution; throw error; diff --git a/tests/focused-deconstruct-boundary.test.js b/tests/focused-deconstruct-boundary.test.js index 6df9def..00cfe23 100644 --- a/tests/focused-deconstruct-boundary.test.js +++ b/tests/focused-deconstruct-boundary.test.js @@ -9,6 +9,10 @@ import { afterEach, beforeEach, describe, it, expect, vi } from "vitest"; import { focusedDeconstructJsonSchema, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation"; +vi.mock("@/lib/supabase/api-auth.js", () => ({ + withAuthenticatedApi: (handler) => handler, +})); + // ── helpers ────────────────────────────────────────────────────────────── function makeMockProvider(inventedTargetNodeId) { @@ -389,6 +393,36 @@ describe("focused-deconstruct targetNodeId identity boundary", () => { } }); + it("returns a sanitized 503 when the provider is unavailable", async () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.doMock("@/lib/llm/provider", () => ({ + getProvider: () => ({ + generateReconstruction: vi.fn().mockRejectedValue(Object.assign(new Error( + "Ollama /api/generate request timed out after 5 minutes", + ), { code: "PROVIDER_UNAVAILABLE", providerApiPath: "/api/generate" })), + }), + getProviderModelName: () => "configured-model", + })); + const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); + + try { + const response = await POST(new Request("http://localhost/api/focused-investigation/deconstruct", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetNodeId: "node-id", targetLabel: "label", targetDescription: "description", + centralStatement: "central", question: "question?", answer: "answer.", + }), + })); + expect(response.status).toBe(503); + const json = await response.json(); + expect(json).toEqual({ error: "Reasoning service is temporarily unavailable." }); + expect(JSON.stringify(json)).not.toMatch(/ollama|generate|timed out/i); + } finally { + errorSpy.mockRestore(); + } + }); + it("preserves the 502 validation-failure contract with diagnostics", async () => { vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({