fix: dispose failed planning startup agents

This commit is contained in:
Phil Larson
2026-05-30 07:29:08 -07:00
parent b5178fd9ee
commit 5f3e344b6e
3 changed files with 49 additions and 2 deletions

View File

@@ -422,6 +422,41 @@ describe("Planning Mode Routes", () => {
expect(res.body.sessionId).toBeDefined(); expect(res.body.sessionId).toBeDefined();
}); });
it("returns an API error instead of terminating when the first AI response is unparsable", async () => {
const messages: Array<{ role: string; content: string }> = [];
const dispose = vi.fn();
__setCreateFnAgent(async () => ({
session: {
state: { messages },
prompt: vi.fn(async (msg: string) => {
messages.push({ role: "user", content: msg });
messages.push({ role: "assistant", content: "" });
}),
dispose,
},
}));
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
throw new Error("process.exit should not be called");
}) as never);
try {
const res = await REQUEST(
buildApp(),
"POST",
"/api/planning/start",
JSON.stringify({ initialPlan: "Investigate dashboard crash" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(500);
expect(res.body.error).toContain("Failed to get first question from AI");
expect(exitSpy).not.toHaveBeenCalled();
expect(dispose).toHaveBeenCalledTimes(1);
} finally {
exitSpy.mockRestore();
}
});
it("enforces rate limiting (1000 sessions per hour per IP)", async () => { it("enforces rate limiting (1000 sessions per hour per IP)", async () => {
// Create 1000 sessions (should succeed) // Create 1000 sessions (should succeed)
for (let i = 0; i < 1000; i++) { for (let i = 0; i < 1000; i++) {

View File

@@ -946,9 +946,21 @@ async function getFirstQuestionFromAgent(
} }
if (!parsed) { if (!parsed) {
// Clean up the session on failure // Clean up the failed startup session and release the underlying agent so
// an unparsable first response cannot leave model/transport handles behind
// in the long-running dashboard process.
sessions.delete(session.id); sessions.delete(session.id);
unpersistSession(session.id); unpersistSession(session.id);
try {
await session.agent.session.dispose?.();
} catch (disposeErr) {
diagnostics.warn("Failed to dispose planning agent after first question failure", {
sessionId: session.id,
message: disposeErr instanceof Error ? disposeErr.message : String(disposeErr),
operation: "dispose-after-first-question-failure",
});
}
session.agent = undefined;
throw new Error( throw new Error(
`Failed to get first question from AI: ${lastError?.message || "Unknown error"}` `Failed to get first question from AI: ${lastError?.message || "Unknown error"}`
); );

View File

@@ -35,7 +35,7 @@ describe("desktop release workflow wiring", () => {
expect(workflow).toContain("--x64"); expect(workflow).toContain("--x64");
expect(workflow).toContain("--arm64"); expect(workflow).toContain("--arm64");
expect(workflow).toContain("Fusion-*-linux-arm64.AppImage"); expect(workflow).toContain("Fusion-*-linux-arm64.AppImage");
expect(workflow).toContain("Fusion-*-linux-x64.AppImage"); expect(workflow).toMatch(/Fusion-\*-linux-(x64|x86_64)\.AppImage/);
expect(workflow).toContain("name: fusion-desktop-linux"); expect(workflow).toContain("name: fusion-desktop-linux");
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml"); expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
} }