type StoredExecutionState = 'pending' | 'complete' | 'failed' | 'cancelled' | 'interrupted'; /** Validated Initiative selector + optional Task reference + linkage authorization, supplied at * admission for a linked Execution. Persisted on the execution row so the terminal CAS can * read it back without the caller re-supplying it (and so it survives a daemon restart — * boot reconciliation's `interrupt()` call goes through the same terminal path). Shape mirrors * the Initiative selector union (`{ uuid }` or `{ human_key }`); kept structurally loose here * because the Zod-validated wire schema lives at the transport boundary (Task I-6), not in * this durable store. `task_uuid` is OPTIONAL (frozen interface, AC-1.1): its absence marks * Initiative-only linkage — there is no Task to scope writes to, check membership against, or * transition. */ export interface ExecutionLinkage { initiative: { uuid: string; } | { human_key: string; }; task_uuid?: string; authorized_by: string; } /** One outbox row: durable proof that a linked terminal Execution needs an Initiative-side * replay (Task I-5's `InitiativeLinker`). `payload` bundles the linkage plus the terminal * envelope so replay never has to re-read `executions` for it. */ export interface OutboxRecord { executionId: string; payload: unknown; createdAt: number; consumedAt: number | null; } interface ExecutionRecord { id: string; type: string; cwd: string; /** Resolved Method captured atomically with admission; retained through crashes before a * terminal envelope exists. */ method: string | null; state: StoredExecutionState; createdAt: number; updatedAt: number; terminalAt: number | null; /** Terminal rows are pruned once now > expiresAt (mirrors the registry TTL). */ expiresAt: number | null; cancellationRequestedAt: number | null; /** Terminal result envelope (JSON), exactly what GET /execution/:id returns. */ resultJson: string | null; /** Pid of the daemon that owns/owned this execution — reconciliation only * touches rows whose owning daemon is no longer alive. */ daemonPid: number; /** Leader pid of the detached codex worker process group, when one spawned. * POSIX: also the process-group id (detached ⇒ group leader). Used by boot * reconciliation to terminate stragglers that outlived a crashed daemon. */ workerPid: number | null; } export declare class ExecutionStore { private readonly db; private readonly ttlMs; /** Set by close(). Executions are detached async work, so one can still be finishing when * the daemon (or a test harness) shuts the store down. Writing to a closed DatabaseSync * throws ERR_INVALID_STATE from a promise nobody awaits — an unhandled rejection that * crashes the process on a path where there is nothing left to persist to anyway. After * close, durable writes become no-ops; every other DB error still propagates. */ private closed; constructor(opts: { dbPath: string; ttlMs: number; }); /** Persist the admission record. Called BEFORE the handle is returned to the * caller, so a handle that exists is always a handle that survives. * * `linkage`, when supplied, marks this Execution as linked to an Initiative Task. It is * persisted here (not held in memory) so the terminal CAS in `terminalize` — reached later, * possibly after a daemon restart via boot reconciliation's `interrupt()` — can read it back * without the caller re-supplying it. Omitting it (the existing unlinked call shape) means * the terminal write for this execution creates no outbox row. * * SPEC-003 B6 round-2 defect B (Option 1): `ExecutionRuntime.submit()` always supplies an * already-validated `linkage` HERE, in the same write as admission — never in a later, * separate call once some downstream Task transition succeeds. Attaching it any later leaves a * crash window where a Task mutation lands in `initiatives.db` but the pending row in * `executions.db` never learns about it, so `terminalize()`'s outbox insert (gated on * `linkage_json`) never fires and the Task is stranded with nothing left to reopen it. */ admit(id: string, type: string, cwd: string, daemonPid: number, linkage?: ExecutionLinkage, method?: string | null): void; /** Record the detached worker's process-group leader pid (codex). Only * meaningful while pending — a terminal row's worker is already reaped. */ recordWorkerPid(id: string, workerPid: number): void; /** Set the cancellation-requested flag (not a state transition). Idempotent; * a terminal row is untouched. */ requestCancel(id: string): void; /** Terminal CAS: only a pending row transitions; a row that already reached * a terminal state is never overwritten (first writer wins). * * Linked executions (admitted with `linkage`) get exactly one outbox row inserted in the * SAME transaction as the terminal update — a CAS that loses the race (row already terminal) * inserts none, and a SQLite failure rolls back both writes. Unlinked executions are * unaffected: no linkage_json means no outbox row, same result as before this table existed. */ private terminalize; complete(id: string, resultJson: string): boolean; fail(id: string, resultJson: string): boolean; cancel(id: string, resultJson: string): boolean; get(id: string): ExecutionRecord | undefined; /** Non-terminal rows owned by daemons other than `ownPid`. Boot-time input * to reconciliation: rows whose owning daemon is dead get fenced + * interrupted; rows owned by a still-alive daemon are left alone. */ stalePending(ownPid: number): ExecutionRecord[]; /** Reconciliation transition: pending → interrupted (CAS, same discipline). */ interrupt(id: string, resultJson: string): boolean; /** * Rows marked `interrupted` at or after `since`. * * Exists for `mma restart` / `mma update`, which must tell the user what the * restart just destroyed. That report has to happen at restart time: terminal * rows expire after `batchTtlMs` (one hour by default) and are pruned at the * next boot, so a user who thinks to look later may find nothing. * * It returns identity only — id, type, cwd — because that is all the table * holds. Admission never stored the prompt, target or options * (`application/execution-runtime.ts`), so nothing here can rebuild the * request. The caller retries; MMA cannot. */ interruptedSince(since: number): ExecutionRecord[]; /** Outbox rows not yet consumed, oldest first. Task I-5's `InitiativeLinker` replays these — * at boot (after fencing) and after every later terminal write. No retention/deletion * behavior here: rows persist until `markOutboxConsumed` marks them, and are never pruned. */ listUnconsumedOutbox(): OutboxRecord[]; /** Conditional consumed marker: only an unconsumed row transitions (idempotent — a replay * that lands after another replay already marked it does nothing and returns false). */ markOutboxConsumed(executionId: string): boolean; /** * Drop terminal execution rows past their retention TTL, and CONSUMED outbox rows past the * same window. Returns execution rows removed. * * The outbox half was missing. `listUnconsumedOutbox` is the only reader and it filters * consumed rows out, so a consumed row is read by nothing — yet nothing deleted them either, * so every linked Execution left one permanent row in a file that is never vacuumed. Bounded * here rather than at consumption time so a replay that has just marked a row still leaves a * forensic window equal to the one terminal executions get. * * UNCONSUMED rows are deliberately never pruned, however old: the row IS the durable promise * that a linked Initiative still needs its terminal replay, and its payload carries everything * that replay needs, so it survives its execution row being pruned. */ pruneExpired(now?: number): number; close(): void; } export {}; //# sourceMappingURL=execution-store.d.ts.map