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

@@ -339,15 +339,7 @@ export async function createSubtaskSession(initialDescription: string, _store?:
persistSubtaskSession(session, "generating");
const cwd = rootDir ?? process.cwd();
generateSubtasks(sessionId, cwd).catch((err) => {
const existing = sessions.get(sessionId);
if (!existing) return;
existing.status = "error";
existing.error = err instanceof Error ? (err.message || "Unknown error") : "Failed to generate subtasks";
existing.updatedAt = new Date();
persistSubtaskSession(existing, "error", existing.error);
subtaskStreamManager.broadcast(sessionId, { type: "error", data: existing.error });
});
void startSubtaskGeneration(sessionId, cwd);
return {
sessionId,
@@ -358,6 +350,20 @@ export async function createSubtaskSession(initialDescription: string, _store?:
};
}
async function startSubtaskGeneration(sessionId: string, cwd: string): Promise<void> {
try {
await generateSubtasks(sessionId, cwd);
} catch (err) {
const existing = sessions.get(sessionId);
if (!existing) return;
existing.status = "error";
existing.error = err instanceof Error ? (err.message || "Unknown error") : "Failed to generate subtasks";
existing.updatedAt = new Date();
persistSubtaskSession(existing, "error", existing.error);
subtaskStreamManager.broadcast(sessionId, { type: "error", data: existing.error });
}
}
async function generateSubtasks(sessionId: string, cwd: string): Promise<void> {
const session = sessions.get(sessionId);
if (!session) throw new SessionNotFoundError(`Subtask session ${sessionId} not found`);
@@ -466,6 +472,48 @@ function completeSession(sessionId: string, subtasks: SubtaskItem[]): void {
subtaskStreamManager.broadcast(sessionId, { type: "complete" });
}
function disposeSubtaskAgentForRetry(session: SubtaskInternalSession): void {
try {
session.agent?.session?.dispose?.();
} catch {
// ignore cleanup errors
}
session.agent = undefined;
}
export async function retrySubtaskSession(sessionId: string, rootDir: string): Promise<void> {
const visibleSession = getSubtaskSession(sessionId);
if (!visibleSession) {
throw new SessionNotFoundError(`Subtask session ${sessionId} not found or expired`);
}
const persisted = _aiSessionStore?.get(sessionId);
if (persisted && persisted.type !== "subtask") {
throw new SessionNotFoundError(`Subtask session ${sessionId} not found or expired`);
}
const session = sessions.get(sessionId);
if (!session) {
throw new SessionNotFoundError(`Subtask session ${sessionId} not found or expired`);
}
const inErrorState = persisted ? persisted.status === "error" : visibleSession.status === "error";
if (!inErrorState) {
throw new InvalidSessionStateError(`Subtask session ${sessionId} is not in an error state`);
}
disposeSubtaskAgentForRetry(session);
session.status = "generating";
session.error = undefined;
session.subtasks = [];
session.thinkingOutput = "";
session.updatedAt = new Date();
persistSubtaskSession(session, "generating");
await startSubtaskGeneration(sessionId, rootDir);
}
export function getSubtaskSession(sessionId: string): SubtaskSession | undefined {
const inMemory = sessions.get(sessionId);
if (inMemory) {
@@ -524,3 +572,10 @@ export class SessionNotFoundError extends Error {
this.name = "SessionNotFoundError";
}
}
export class InvalidSessionStateError extends Error {
constructor(message: string) {
super(message);
this.name = "InvalidSessionStateError";
}
}