/** * Graph Execution Engine v2 — Startup Recovery Sweep * * Version: 1.0 * Date: 2026-07-25 * * The plugin-startup counterpart to {@link recover()}. On every plugin reload / * process restart, the platform sweeps the on-disk engine-state store * (`.rolebox/state/engine-*.json`) and resumes every graph that was left * mid-execution by a crash. This module is the *orchestrating loop* that walks * the store; the per-graph mechanics live in `engine-recovery.ts` * (`EngineRuntime.recover()`). * * Contract (engine-state-machine.md §5.1): * - Scan the `.rolebox/state` directory under `directory` for `engine-*.json`. * - Parse each via `loadEngineStateFromJson` (version-gated, tolerant of a * corrupt / version-mismatched file → `null`). * - **Skip** graphs whose phase is already `complete` — a terminal graph has * nothing to resume. * - For every remaining graph, build `createEngine(declaration, { manager, * graphId, stateDir, onNodeCompletion, onGraphTerminal, graphEvents })` and * `await recover()` **inside a per-graph * try/catch**, so one corrupt or failing graph never aborts the sweep. * Failures are captured in `failed[]` and the loop continues to the sibling. * * Observer seams (monitor S10): the optional `onNodeCompletion` / * `onGraphTerminal` / `graphEvents` options are forwarded to every resumed * engine, so a graph that finished while the plugin was down re-announces its * transitions and continues its durable event log instead of being silent. * Absent → resumed engines behave exactly as before. * * Failure isolation is a hard guarantee: plugin startup must never be blocked * by a single bad engine file. This is enforced at three levels: * 1. A missing state dir → clean no-op (`scanned: 0`). * 2. `loadEngineStateFromJson` returns `null` (corrupt/version-mismatch) → * the file is captured in `failed[]`, not thrown. * 3. `recover()` per-graph try/catch → a throwing graph is captured and the * remaining graphs still recover. * * Idempotency: this sweep is safe to call repeatedly, but it must not be run * CONCURRENTLY — two overlapping sweeps would recover the same graph twice. * `recover()` reconciles every persisted `running` node against the dispatch * system: a still-live task is re-attached (never re-dispatched), a vanished * task is timed out, and a task that finished during the restart window has * its terminating signal re-emitted. It then rebuilds the frontier from the * `ready` nodes and DISPATCHES them — so "never re-dispatches" holds only for * nodes that are already running. A second sweep after a successful pass * finds every graph already `complete` and skips it (the phase is persisted). * * Design references: * - `.rolebox/design/engine-state-machine.md` §5 (resilience / crash recovery), * §5.1 (recovery entry point), §5.2 (idempotency). * - `src/pi-extension.ts` loop-recovery block (the plugin-startup pattern this * mirrors). * - Pattern mirrored from `src/loop/loop-store.ts` / `src/utils/state-paths.ts` * (`.rolebox/state` layout — pattern reference only, those files are not * modified). */ import type { DispatchManager } from "../../dispatch/core/manager.ts"; import type { NodeCompletionEvent, GraphTerminalEvent } from "./engine-advance.ts"; import type { GraphEventRecorder } from "./graph-events.ts"; /** Outcome of a startup recovery sweep over the on-disk engine store. */ export interface RecoveryStartupReport { /** Total number of `engine-*.json` files found in the state store. */ scanned: number; /** Graphs successfully resumed via {@link createEngine} + `recover()`. */ recovered: number; /** * Files/graphs that could not be recovered, each labelled with the * underlying `engine-*.json` filename (and the graph id when extractable). * Never empty a sweep — it is a diagnostic log, not a blocker. */ failed: string[]; } /** Options for {@link recoverInterruptedGraphs}. */ export interface RecoverInterruptedGraphsOptions { /** * Workspace directory whose `.rolebox/state/` store is swept for * `engine-*.json` files. This is the same directory `EnginePersistence` * writes to (`join(directory, ".rolebox", "state")`). */ directory: string; /** * The live {@link DispatchManager} every resumed engine reconciles its * `running` nodes against (`getTask` / `onTaskTerminated`). */ manager: DispatchManager; /** * Hard on/off switch for the sweep. When `false`, returns a no-op report * (`{ scanned: 0, recovered: 0, failed: [] }`) without touching the store. * Defaults to `true`. */ enabled?: boolean; /** * Optional workspace directory passed through to {@link createEngine} for * the resumed engine's persistence seam. Defaults to `directory` — they are * the same thing unless a caller deliberately separates the scan root from * the re-persist root. */ stateDir?: string; /** * Optional node-completion notification seam (graph monitoring) forwarded to * every resumed engine's `onNodeCompletion` hook. Recovery re-emits the * terminating transitions of nodes whose tasks finished during the restart * window — wiring a notifier here lets the orchestrator perceive those * completions instead of the recovered engine being completely silent on * reminders (the pre-fix gap). Defaults to absent → each resumed engine * keeps its default no-op seam, so behavior is identical to older versions. */ onNodeCompletion?: (event: NodeCompletionEvent) => void | Promise; /** * Optional graph-terminal notification seam (graph monitoring) forwarded to * every resumed engine's `onGraphTerminal` hook — re-announces * [GRAPH COMPLETE] / [GRAPH BLOCKED] for graphs that reached a terminal * state while the plugin was down. Defaults to absent → no-op (unchanged * behavior). */ onGraphTerminal?: (event: GraphTerminalEvent) => void | Promise; /** * Optional write-side durable event log (graph monitoring) forwarded to * every resumed engine's `graphEvents` seam. Passing the same * {@link GraphEventRecorder} (built over the same `stateDir`) the running * graph used lets a recovered engine CONTINUE appending * `node_completed` / `phase_change` / … lines to * `graph-events-{hash}.ndjson` instead of leaving the audit log silent * after a restart. Defaults to absent → no event logging (unchanged * behavior). */ graphEvents?: GraphEventRecorder; } /** * Sweep the on-disk engine-state store and resume every interrupted graph. * * Walks `.rolebox/state/engine-*.json` under `directory`, parses each via the * version-gated `loadEngineStateFromJson` helper, skips already-`complete` * graphs, and resumes the rest — each in its own try/catch so a single bad * file never aborts the sweep (see the module docs for the three-level failure * isolation). * * Idempotent and callable any number of times (a resumed graph is persisted as * `complete` and skipped on the next pass). * * @param opts The sweep configuration (directory + manager are required). * @returns A {@link RecoveryStartupReport} describing what was scanned, * recovered, and failed. */ export declare function recoverInterruptedGraphs(opts: RecoverInterruptedGraphsOptions): Promise; //# sourceMappingURL=engine-startup.d.ts.map