/** * Graph Engine — Write-Side Durable Graph Event Log * * A best-effort, append-only JSON-lines event log for a graph execution * instance. {@link GraphEventRecorder} records the write-side transitions an * engine performs — node dispatch, node terminal transition, engine phase * change, and budget update — so a running graph can be audited / replayed * after the fact from `.rolebox/state/graph-events-{hash}.ndjson`. * * Durability contract: * * - **Append-only.** One JSON line per event, written synchronously via * `appendFileSync`. Each event is a single, complete line (the JSON record + * a newline in one write call) so a reader never observes a half-written * line from this writer. * - **Total.** A recorder never throws. Any write failure (missing/uncreatable * directory, filesystem error, corrupt path) is contained and the engine * proceeds — this is observability, never a control path. `JSON.stringify` * on a JSON-safe record cannot throw. Containment is NOT silence (Y22): the * failure is counted and logged (first failure, then every Nth repeat) so a * permanently degraded audit log is distinguishable from an idle one. * - **No-op-safe.** With no `stateDir` configured no recorder is constructed, * so there is no file activity and the engine behaves exactly as before. * * The phase-change and budget-event sinks live in `engine-state.ts` * (`setPhaseEventSink` / `setBudgetEventSink`) so the engine's pure * `transitionPhase` / `applyBudgetDelta` can reach the recorder without an * import cycle. Constructing a recorder registers those sinks; a recorder is * the only writer, so the module-level sink is last-writer-wins (single-engine * execution). * * Writes mirror the `engine-persistence.ts` philosophy (write-through, never * breaks advancement) but use `appendFileSync` for the log instead of a * `.tmp`+`rename` snapshot, because an event log must *accumulate* rather than * overwrite. */ import type { EnginePhase } from "../../constants.ts"; import type { GraphBudgetState } from "../../types.engine-v2.ts"; import type { NodeCompletionEvent } from "./engine-advance.ts"; /** * The kinds of write-side graph events this log records. * * - `node_dispatched` — a ready node became `running` and was launched. * - `node_completed` — a node reached a terminal / notable transition * (`answer` / `revise_needed` / `escalate` / `timeout`, carrying `nodeStatus`). * - `phase_change` — the engine lifecycle advanced `idle → executing → complete`. * - `budget_update` — graph-level cumulative budget counters were updated. * - `notification_degraded` — a graph-notify seam could not resolve an emperor * session, so a notification was suppressed (F6 degradation marker, written * by the toolset's handler resolver). */ export declare const GRAPH_EVENT_TYPES: readonly ["node_dispatched", "node_completed", "phase_change", "budget_update", "notification_degraded"]; /** The union of {@link GRAPH_EVENT_TYPES}. */ export type GraphEventType = (typeof GRAPH_EVENT_TYPES)[number]; /** * One JSON line in the event log. Optional fields (`?`) are omitted from the * serialized line when undefined (JSON-safe): a `node_dispatched` line has no * `signalType`/`completedAt`, a `phase_change` line has no `nodeId`, etc. */ export interface GraphEventRecord { /** Epoch-ms timestamp when the event was recorded. */ ts: number; /** Owning graph id. */ graphId: string; /** Node id — present for node-scoped events (`node_dispatched` / `node_completed`). */ nodeId?: string; /** The event kind. */ event: GraphEventType; /** * A generic status: the target `EnginePhase` for `phase_change`, the * `NodeStatus` for `node_completed`, `"running"` for `node_dispatched`. */ status?: string; /** The terminating signal for `node_completed` (`answer` / `revise_needed` / …). */ signalType?: string; /** The node's bound agent id (node-scoped events). */ agent?: string; /** When the node started (node-scoped events). */ startedAt?: number; /** When the node completed (node_completed). */ completedAt?: number; /** The cumulative graph budget snapshot for `budget_update`. */ budget?: GraphBudgetState; } /** * Derive the stable per-graph event-log filename fragment from a `graphId`. * * The hash (rather than the raw graph id) keeps the filename short and free of * any id characters that are unsafe on disk; hashing the graph id (not a random * nonce) means the same graph maps to the same file across a crash / recovery, * which is exactly what a durable audit log wants. */ export declare function graphEventsHash(graphId: string): string; /** Absolute path to a graph's event log: `.rolebox/state/graph-events-{hash}.ndjson`. */ export declare function graphEventsPath(directory: string, graphId: string): string; /** * Read-only, total event-log reader for ONE graph. Reads the graph's durable * event log (`.rolebox/state/graph-events-{hash}.ndjson`) and returns the * parsed records in file order. Never throws: a missing file, an unreadable * file, or a corrupt line degrades to an empty / partial result rather than an * error — observability is never a control path (mirrors the recorder's total * discipline). The per-graph path derivation already scopes the read to the * requested graph; records whose `graphId` does not match are additionally * skipped defensively. */ export declare function readGraphEventLog(directory: string, graphId: string): GraphEventRecord[]; /** * Append-only, total JSON-lines event recorder for a single graph instance. * * Construct with a workspace directory (defaults to `process.cwd()`); the log * lives under `.rolebox/state/`. The recorder registers itself as the * phase-change and budget-event sinks in `engine-state.ts` so the engine's * pure transition functions can reach it. All public methods are total — * none of them throw, so wiring this into the engine can never break graph * advancement (identical discipline to `EnginePersistence.save`). */ export declare class GraphEventRecorder { private readonly directory; /** * Consecutive failed appends (Y22). Non-zero means the audit log is * DEGRADED — events are being dropped. Reset by the next successful write; * the count throttles the degradation log line. */ private writeFailures; constructor(directory?: string); /** Consecutive failed event-log appends since the last successful write (Y22). */ get consecutiveWriteFailures(): number; /** Whether the audit log is currently degraded (≥1 consecutive write failure). */ get writeDegraded(): boolean; /** Record that a node was dispatched (ready → running, launched). */ nodeDispatched(graphId: string, nodeId: string, agent: string, startedAt?: number): void; /** * Record a node's terminal / notable transition, derived directly from the * engine's {@link NodeCompletionEvent} (the same immutable facts the * `onNodeCompletion` notifier seam carries). Covers `answer` / * `revise_needed` / `escalate` / `timeout` with the node's lifecycle status. */ nodeCompleted(event: NodeCompletionEvent): void; /** Record an engine lifecycle phase transition (`idle → executing → complete`). */ phaseChange(graphId: string, _from: EnginePhase, to: EnginePhase): void; /** Record a graph-level cumulative budget update (snapshot of the counters). */ budgetUpdate(graphId: string, budget: GraphBudgetState): void; /** * Record that a graph notification seam degraded: the emperor session could * not be resolved, so a graph-notify reminder was suppressed (F6). `kind` * names the suppressed seam (`completion` / `terminal` / `stall`, Y32) and * rides in the record's generic `status` string slot. The stall kind is * accepted here rather than asserted into a two-value union at the toolset * call site. Optional-additive — only written when a stateDir / recorder * exists; absent stateDir → warning log only. */ notificationDegraded(graphId: string, kind: "completion" | "terminal" | "stall"): void; /** * Append one event line. Creates `.rolebox/state/` on demand and appends a * single complete line (`JSON.stringify(record) + "\n"`). Never throws: every * failure path (mkdir, append) is contained so a disk problem degrades to a * dropped log line, not a broken engine. JSON-safe records mean stringify * cannot throw. * * Y22: containment is observable. A failure increments the consecutive * counter and logs the first failure (then every 20th) with the underlying * error, so a full disk / permission error produces a visible degradation * marker instead of an audit log that is silently empty forever. */ private _append; } //# sourceMappingURL=graph-events.d.ts.map