feat(FN-1152): add retry flows for failed AI planning sessions

- Keep errored AI sessions retryable in the session store and add backend retry handlers for planning, subtask breakdown, and mission interview flows
- Add retry API routes and dashboard API client helpers for planning, subtask, and mission interview session retries
- Update PlanningModeModal, SubtaskBreakdownModal, MissionInterviewModal, and background session handling to show error states with retry/cancel UX
- Expand unit and integration tests across store, services, routes, and modal components to cover retry success and failure paths
This commit is contained in:
gsxdsm
2026-04-08 15:10:46 -07:00
parent d0578a589b
commit 0ff6d42c0f
20 changed files with 1368 additions and 234 deletions

View File

@@ -1400,6 +1400,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
"POST /planning/start",
"POST /planning/start-streaming",
"POST /planning/respond",
"POST /planning/:sessionId/retry",
"POST /planning/cancel",
"POST /planning/create-task",
"POST /planning/start-breakdown",
@@ -5862,6 +5863,31 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
});
router.post("/subtasks/:sessionId/retry", async (req, res) => {
try {
const { sessionId } = req.params;
if (!sessionId || typeof sessionId !== "string") {
throw badRequest("sessionId is required");
}
const scopedStore = await getScopedStore(req);
const { retrySubtaskSession } = await import("./subtask-breakdown.js");
await retrySubtaskSession(sessionId, scopedStore.getRootDir());
res.json({ success: true, sessionId });
} catch (err: any) {
if (err instanceof ApiError) {
throw err;
}
if (err.name === "SessionNotFoundError") {
throw notFound(err.message);
} else if (err.name === "InvalidSessionStateError") {
throw badRequest(err.message);
} else {
rethrowAsApiError(err, "Failed to retry subtask session");
}
}
});
/**
* POST /api/planning/start
* Start a new planning session.
@@ -5988,6 +6014,31 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
});
router.post("/planning/:sessionId/retry", async (req, res) => {
try {
const { sessionId } = req.params;
if (!sessionId || typeof sessionId !== "string") {
throw badRequest("sessionId is required");
}
const scopedStore = await getScopedStore(req);
const { retrySession } = await import("./planning.js");
await retrySession(sessionId, scopedStore.getRootDir());
res.json({ success: true, sessionId });
} catch (err: any) {
if (err instanceof ApiError) {
throw err;
}
if (err.name === "SessionNotFoundError") {
throw notFound(err.message);
} else if (err.name === "InvalidSessionStateError") {
throw badRequest(err.message);
} else {
rethrowAsApiError(err, "Failed to retry planning session");
}
}
});
/**
* POST /api/planning/cancel
* Cancel and cleanup a planning session.