feat(coder): boulder state — cross-session plan persistence + auto-resumption
New plans table (id, project_id, title, description, status, flow_run_id, progress_pct, items_total, items_completed, metadata, timestamps) with CHECK constraints and indexes. Plan store (plan-store.ts): createPlan, getPlan, listPlans, listActivePlans, updatePlan, updatePlanFromRun, findPlanWithRunningRun, planStatusFromRun. Flow-runner integration: onRunTerminal callback fires on every terminal transition (complete/fail/cancel) and updates linked plans automatically. 5 API endpoints: GET /api/plans, GET /api/plans/active, GET /api/plans/:id, POST /api/plans, PATCH /api/plans/:id. 484 tests pass, build clean.
This commit is contained in:
@@ -89,6 +89,8 @@ interface Deps {
|
||||
broker: Broker;
|
||||
log: FastifyBaseLogger;
|
||||
config: Config;
|
||||
/** Fired when a flow run reaches a terminal state (for plan-store integration). */
|
||||
onRunTerminal?: (runId: string, status: 'completed' | 'failed' | 'cancelled') => void;
|
||||
}
|
||||
|
||||
interface FlowStepRow {
|
||||
@@ -479,6 +481,7 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
WHERE id = ${runId} AND status = 'running'
|
||||
`;
|
||||
if (updated.count === 0) return; // already terminal (e.g. cancelled) — don't publish
|
||||
deps.onRunTerminal?.(runId, 'completed');
|
||||
publishStep(runId, lastAgentStepId(flow, input, model), 'completed', {
|
||||
run_status: 'completed',
|
||||
report,
|
||||
@@ -498,6 +501,7 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
WHERE id = ${runId} AND status = 'running'
|
||||
`;
|
||||
if (updated.count === 0) return;
|
||||
deps.onRunTerminal?.(runId, 'failed');
|
||||
const stepId = failedStepId ?? (flow ? lastAgentStepId(flow, input, model) : 'run');
|
||||
log.warn({ runId, error }, 'flow-runner: run failed');
|
||||
await appendStepEvent(sql, runId, stepId, 'failed', { error });
|
||||
@@ -512,6 +516,7 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
WHERE id = ${runId} AND status = 'running'
|
||||
`;
|
||||
if (updated.count === 0) return; // idempotent — already terminal
|
||||
deps.onRunTerminal?.(runId, 'cancelled');
|
||||
// Any remaining pending steps are unreachable; mark + publish them so the
|
||||
// pane can show them as cancelled rather than stuck in pending.
|
||||
const pending = await sql<{ step_id: string; kind: string }[]>`
|
||||
@@ -742,6 +747,7 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
WHERE id = ${runId} AND status = 'running'
|
||||
`;
|
||||
if (updated.count === 0) return { cancelled: false, taskIds: [] };
|
||||
deps.onRunTerminal?.(runId, 'cancelled');
|
||||
|
||||
// Mark all non-terminal steps cancelled and collect in-flight task_ids.
|
||||
const steps = await sql<{ step_id: string; task_id: string | null; kind: string }[]>`
|
||||
|
||||
Reference in New Issue
Block a user