/** * F1 — per-task abort registry. A Stop on an external-agent task must reach the * in-flight run and abort its child / prompt. Each external run-function registers * its per-turn AbortController here keyed by task id; the cancel route calls * `cancel(taskId)` to fire it; the run-function's `.finally` deletes the entry. * * Idempotent by construction: * - `cancel()` on an already-aborted controller no-ops (AbortController.abort() * is idempotent) → a rapid double-Stop is safe. * - `cancel()` on an unknown / already-finished task returns false → a * cancel-after-natural-exit (entry already deleted) and a Stop on a native * boocode task (never registered) are both safe no-ops. * * Pure (no DB / child / IO) so the abort wiring + idempotency contract is * unit-testable in isolation — mirrors the turn-guard / lifecycle-decisions * pure-helper precedent. */ export interface CancelRegistry { /** Create + store an AbortController for this task, returning it for the run. */ register(taskId: string): AbortController; /** Abort the task's in-flight run. Returns false when no controller is registered. */ cancel(taskId: string): boolean; /** Drop the task's entry (called from the run's `.finally`). No-op if absent. */ delete(taskId: string): void; /** Whether a controller is currently registered for this task. */ has(taskId: string): boolean; } export function createCancelRegistry(): CancelRegistry { const controllers = new Map(); return { register(taskId) { const ac = new AbortController(); controllers.set(taskId, ac); return ac; }, cancel(taskId) { const ac = controllers.get(taskId); if (!ac) return false; ac.abort(); return true; }, delete(taskId) { controllers.delete(taskId); }, has(taskId) { return controllers.has(taskId); }, }; }