mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
Phase 6.1 of the Critique Theater rollout: a single new endpoint and the in-process registry that backs it. POST /api/projects/:projectId/critique/:runId/interrupt cascades an AbortController to the orchestrator that owns the spawned CLI so the parser can flush best-so-far state and emit critique.interrupted before the process exits. Backed by a new in-process run registry that the orchestrator wiring registers each run into before runOrchestrator is invoked, and unregisters in a finally block. The registry is keyed by (projectId, runId), not just runId. A request to interrupt project p1's runId cannot find or abort a registry handle that belongs to project p2 even if their ids ever collide. The HTTP handler also performs its own DB-row projectId check before calling the registry, so cross-project leakage is blocked at two layers. The endpoint is idempotent on already-interrupted rows: a client that lost the first response and retries observes 202 with prevStatus "interrupted" rather than a 409 conflict. Other terminal statuses (shipped, failed, timed_out, degraded, below_threshold, legacy) still return 409 because those runs reached their real terminal state on their own and an interrupt is no longer meaningful. Recovery path for stale running rows: when registry.interrupt returns false (the in-process registry has no AbortController for this projectId/runId pair) but the DB still says 'running', the endpoint marks the row 'interrupted' directly with recoveryReason='no_live_handle' and returns 202 with recovered=true. This window opens after a daemon restart in the gap before reconcileStaleRuns sees the row old enough. Without the recovery branch the endpoint would lie: 202 accepted, no child signaled, no critique.interrupted event, row stuck running. The new persistence helper markRunInterruptedRecovery mirrors the per-row write reconcileStaleRuns already does, gated on status='running' so a row that just transitioned terminal is not overwritten. Task 6.2 (rerun endpoint) is intentionally not in this PR. The earlier draft conflated row insertion between the handler and runOrchestrator (primary key collision) and did not actually start a new agent spawn. Rerun needs a real chat-run path with prior-art context, an artifact-id validator, and SQL LIKE escaping that the row lookup path is missing today; it is cleaner shipped as a follow-up than wedged into this PR. Tests: - critique-run-registry: 17 cases covering register, get, interrupt, unregister, list, plus the new (projectId, runId) composite key invariants (cross-project register, cross-project get/interrupt isolation, unregister keying). - critique-interrupt-endpoint: 17 cases covering 202 happy path, 404 on unknown run, 404 on cross-project run, 404 cross-project leak guard at the registry layer, 409 on terminal statuses, 202 idempotent retry on already-interrupted, stale-handle defense, 202 + recovered on a stale running row with no live handle, 400 on bad params. Incidental: apps/web/src/i18n/locales/id.ts was missing 18 fileViewer deploy/Cloudflare keys after upstream landed PR #805 (R2 release publishing). Without those keys the workspace web typecheck fails on the i18n Dict equality check, blocking CI on every PR. Added Indonesian translations for the missing keys to unblock. Co-authored-by: Nagendhra <nagendhra405@gmail.com>