fix(dashboard): harden insight extraction response handling

This commit is contained in:
Phil Larson
2026-06-12 15:58:20 -07:00
parent 8541db3185
commit e6eef1a925
3 changed files with 69 additions and 7 deletions

View File

@@ -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.

View File

@@ -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", () => {

View File

@@ -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, {