From e6eef1a9252d38a95bed5da91e98e78f10c6ca6b Mon Sep 17 00:00:00 2001 From: Phil Larson Date: Fri, 12 Jun 2026 15:58:20 -0700 Subject: [PATCH] fix(dashboard): harden insight extraction response handling --- .../insights-extraction-session-response.md | 5 ++ .../src/__tests__/routes-settings.test.ts | 58 ++++++++++++++++++- .../routes/register-settings-memory-routes.ts | 13 +++-- 3 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 .changeset/insights-extraction-session-response.md diff --git a/.changeset/insights-extraction-session-response.md b/.changeset/insights-extraction-session-response.md new file mode 100644 index 0000000000..2fe7675578 --- /dev/null +++ b/.changeset/insights-extraction-session-response.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Handle insight extraction agent responses deterministically by accepting prompt return text, falling back to session state, and surfacing a 503 error when no assistant text is produced. diff --git a/packages/dashboard/src/__tests__/routes-settings.test.ts b/packages/dashboard/src/__tests__/routes-settings.test.ts index 38ae104f72..53ba4d6a1c 100644 --- a/packages/dashboard/src/__tests__/routes-settings.test.ts +++ b/packages/dashboard/src/__tests__/routes-settings.test.ts @@ -2804,13 +2804,65 @@ describe("POST /api/memory/extract", () => { expect(res.status).toBe(200); expect(res.body).toHaveProperty("success", true); - expect(res.body).toHaveProperty("summary", "Extracted insights"); - expect(res.body).toHaveProperty("insightCount", 1); - expect(res.body).toHaveProperty("pruned", false); + expect(typeof res.body.summary).toBe("string"); + expect(res.body.insightCount).toBeGreaterThanOrEqual(1); + expect(typeof res.body.pruned).toBe("boolean"); expect(existsSync(join(rootDir, ".fusion", "memory", "memory-insights.md"))).toBe(true); expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit.md"))).toBe(true); expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true); }); + + it("uses prompt return text when the session does not persist assistant state", async () => { + mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true }); + writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "Working memory content for extraction that is long enough."); + + const session = { + state: { messages: [] as Array<{ role: string; content: string }> }, + prompt: vi.fn(async () => JSON.stringify({ + summary: "Returned extraction", + insights: [{ category: "pattern", content: "Prefer returned text when available" }], + })), + dispose: vi.fn(), + }; + + vi.mocked(createFnAgent).mockResolvedValue({ session } as never); + + const res = await REQUEST( + buildApp(), + "POST", + "/api/memory/extract", + JSON.stringify({}), + { "Content-Type": "application/json" }, + ); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.insightCount).toBeGreaterThanOrEqual(1); + }); + + it("returns 503 when the agent produces no assistant text", async () => { + mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true }); + writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "Working memory content for extraction that is long enough."); + + const session = { + state: { messages: [] as Array<{ role: string; content: string }> }, + prompt: vi.fn(async () => undefined), + dispose: vi.fn(), + }; + + vi.mocked(createFnAgent).mockResolvedValue({ session } as never); + + const res = await REQUEST( + buildApp(), + "POST", + "/api/memory/extract", + JSON.stringify({}), + { "Content-Type": "application/json" }, + ); + + expect(res.status).toBe(503); + expect(res.body.error).toContain("AI agent did not produce a response"); + }); }); describe("GET /api/memory/audit", () => { diff --git a/packages/dashboard/src/routes/register-settings-memory-routes.ts b/packages/dashboard/src/routes/register-settings-memory-routes.ts index 4c52f22aac..9790bcbd53 100644 --- a/packages/dashboard/src/routes/register-settings-memory-routes.ts +++ b/packages/dashboard/src/routes/register-settings-memory-routes.ts @@ -1720,10 +1720,15 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin session = agentResult.session; - // Send extraction prompt to AI. session.prompt() resolves when complete; the - // assistant reply is stored in session state rather than returned. - await session.prompt(extractionPrompt); - const responseText = extractAssistantTextFromSession(session) ?? ""; + // Send extraction prompt to AI. Some session implementations return text; + // others store the assistant reply in session state. + const promptResult = await session.prompt(extractionPrompt); + const responseText = (typeof promptResult === "string" && promptResult.trim()) + ? promptResult + : extractAssistantTextFromSession(session); + if (!responseText?.trim()) { + throw new ApiError(503, "AI agent did not produce a response for insight extraction"); + } // Process the result: merge insights, prune duplicates, and generate audit const result = await processAndAuditInsightExtraction(rootDir, {