/** * POST /api/projects/[projectId]/agent/sessions/[sessionId]/retry * * Re-run a failed or stopped session, optionally with a follow-up instruction. * Resets the session row to `running` and fires the agent-runner again. * * Body: { continueTask?: string } * continueTask — if provided, appended to the original task so the agent * understands what was already tried */ import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; export async function POST( req: Request, { params }: { params: Promise<{ projectId: string; sessionId: string }> } ) { try { const { projectId, sessionId } = await params; const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json().catch(() => ({})) as { continueTask?: string }; // Verify ownership and load the original session const rows = await query<{ id: string; project_id: string; app_name: string; app_path: string; task: string; status: string; }>( `SELECT s.id, s.project_id, s.app_name, s.app_path, s.task, s.status FROM agent_sessions s JOIN fs_projects p ON p.id::text = s.project_id::text JOIN fs_users u ON u.id = p.user_id WHERE s.id = $1::uuid AND s.project_id::text = $2 AND u.data->>'email' = $3 LIMIT 1`, [sessionId, projectId, session.user.email] ); if (rows.length === 0) { return NextResponse.json({ error: "Session not found" }, { status: 404 }); } const s = rows[0]; if (!["failed", "stopped"].includes(s.status)) { return NextResponse.json( { error: `Session is ${s.status} — can only retry failed or stopped sessions` }, { status: 409 } ); } // Fetch giteaRepo from the project const proj = await query<{ data: Record }>( `SELECT data FROM fs_projects WHERE id::text = $1 LIMIT 1`, [projectId] ); const giteaRepo = proj[0]?.data?.giteaRepo as string | undefined; // Clear persisted event timeline so SSE / replay matches the new run (no-op if table missing) try { await query(`DELETE FROM agent_session_events WHERE session_id = $1::uuid`, [sessionId]); } catch { /* table may not exist until admin migrate */ } // Reset the session row so the frontend shows it as running again await query( `UPDATE agent_sessions SET status = 'running', error = NULL, output = '[]'::jsonb, changed_files = '[]'::jsonb, started_at = now(), completed_at = NULL, updated_at = now() WHERE id = $1::uuid`, [sessionId] ); // Re-fire the agent runner fetch(`${AGENT_RUNNER_URL}/agent/execute`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, projectId, appName: s.app_name, appPath: s.app_path, giteaRepo, task: s.task, continueTask: body.continueTask?.trim() || undefined, }), }).catch(err => { console.warn("[retry] runner not reachable:", err.message); query( `UPDATE agent_sessions SET status = 'failed', error = 'Agent runner not reachable', completed_at = now(), updated_at = now() WHERE id = $1::uuid`, [sessionId] ).catch(() => {}); }); return NextResponse.json({ sessionId, status: "running" }); } catch (err) { console.error("[retry POST]", err); return NextResponse.json( { error: "Failed to retry session", details: err instanceof Error ? err.message : String(err) }, { status: 500 } ); } }