/** * Graph Execution Engine v2 — Scoped / Cascade Cancellation Primitive * * Version: 2.0 * Date: 2026-07-26 * * {@link cancelNodes} is a caller-driven cancellation primitive: cancel one or * more named node ids (optionally cascading to their transitive DOWNSTREAM * dependents), distinct from the existing auto-cancellation lanes: * * - `cancelPendingUpstreams` (`cascade-canceller.ts`) retires upstreams a * convergence node no longer needs once its join resolves. * - `pruneDownstreamSubgraph` (`approval-handler.ts`) cancels the rejected * branches' transitive dependents that cannot survive on approved sources * alone during a partial approval. * - `EngineRuntime.cancel()` cancels the WHOLE graph. * * This primitive is the scoped, user-addressed version: it cancels exactly the * ids the caller asks for (not the whole graph), and when `cascade` is set it * also retires every node transitively downstream of those targets over * `graphDeclaration.edges` (forward closure). It reuses ONLY existing lifecycle * machinery — it never invents new transitions or whole-graph teardown: * * 1. Lifecycle: `markCancelled` / `markDone` through the generic * `canTransitionNode` guard (`node-lifecycle.ts`) — identical to the * cascade-canceller and approval-handler retirement pattern. * 2. Cancellable guard: only `pending | ready | running` nodes are retired * (the `isCancellable` rule from `cascade-canceller.ts:167-172`). * `completed`, `blocked`, `escalate`, `timeout`, `cancelled`, `done` are * left untouched (the transition table enforces this by throwing — we * guard before calling). * 3. Forward closure: the BFS-over-edges pattern from * `pruneDownstreamSubgraph` (`approval-handler.ts:211-262`). * 4. Dispatch teardown: `NodeDispatchPort.cancelTask` fire-and-forget (never * awaited) — the `CancelDispatchPort` seam, structurally satisfied by * `NodeDispatchPort` (`engine-advance.ts`). * * Loop-target expansion: when a requested target is a member of a declared * loop group (`node.loopGroupId`), the whole loop group's member set is * cancelled, because a loop is an indivisible bounded cycle — retiring one * member without its partners strands the back-edge. Expansion reads * `graphDeclaration.loop_groups[].nodes`. * * This module is an import-only consumer of the lifecycle state machine and * frontier (`engine-state.ts`). It never dispatches, never touches * `upstreamResults`, and never cancels the whole graph for a scoped target. */ import type { EngineState, NodeRuntimeState } from "../../types.engine-v2.ts"; import type { CancelDispatchPort } from "./cascade-canceller.ts"; /** Options for {@link cancelNodes}. */ export interface CancelScopeOptions { /** * When true, also cancel every node transitively downstream of the expanded * targets over `graphDeclaration.edges` (forward closure). Default false — * cancels only the requested targets. */ cascade?: boolean; } /** * Notification hook invoked for every node actually retired by a scoped * cancellation (monitor H4). Receives the node id and the cancellation reason * AFTER the node's lifecycle advanced to `cancelled → done`. The engine * runtime wires it to the advance engine's `notifyNodeTerminal` seam so a * scoped cancellation is observable to the monitor (completion seam + durable * event log) exactly like signal-driven transitions. Optional — direct * consumers that don't need observation omit it. */ export type CancelNodeNotifier = (nodeId: string, reason: string) => void; /** Result of {@link cancelNodes}, for diagnostics and tests. */ export interface CancelScopeReport { /** * The effective target set after loop-member expansion: the requested ids * plus, for any target that is a loop-group member, that group's full member * set. Deduplicated. */ target: string[]; /** * Node ids actually retired to `cancelled → done` (and, when a cancel seam * was present and the node carried a dispatch task, handed to `cancelTask`). * Includes the targets and — when `cascade` — their transitive downstream. */ cancelled: string[]; /** * Node ids encountered (effective targets and, under `cascade`, downstream * dependents) that were NOT cancellable — already `completed`, `blocked`, or * terminal (`escalate` / `timeout` / `cancelled` / `done`). Left untouched. */ skipped: string[]; /** Dispatch task ids handed to `cancelTask` fire-and-forget (best-effort). */ cancelCalls: string[]; } /** * Expand a requested target set into its effective member set: any target that * is a loop-group member pulls in its loop group's full `nodes[]` member set * (a loop is an indivisible bounded cycle). Deduplicates. */ export declare function expandLoopMembers(state: EngineState, requested: readonly string[]): string[]; /** * Options for {@link retireCancelledNode}. */ export interface RetireCancelledNodeOptions { /** * M10 session-slot refund. When true (default), a `running` node with a live * dispatch task decrements the graph-level `sessionsSpawned` counter * synchronously BEFORE the `cancelled` transition. The dispatch layer reports * the cancellation asynchronously AFTER the node already advanced * `cancelled → done`, so the termination callback's status guard * (`engine-recovery.ts:416`, `current.status !== Running → return`) bails and * its `applyBudgetDelta({sessions: -1})` (`engine-recovery.ts:427-429`) never * fires on this path — the refund must happen here or never. Turn OFF only to * reproduce the no-refund cascade lane behavior (`cancelPendingUpstreams`), * which today retires upstreams without touching the net-live counter. */ refund?: boolean; /** * Optional cancellation seam. When the port provides `cancelTask` and the * node carries a `dispatchTaskId`, the task is handed off fire-and-forget * (`void`, never awaited, never acked). */ dispatchPort?: CancelDispatchPort; /** * Optional collector for the dispatch task ids handed to `cancelTask` * (reporting — the `CancelScopeReport.cancelCalls` source). Only populated * for nodes whose task was actually torn down. */ cancelCalls?: string[]; /** * Optional per-node notification hook (monitor H4) invoked with * `(nodeId, reason)` AFTER the node's lifecycle advanced to * `cancelled → done`. Callers that don't need observation omit it. */ onCancelled?: CancelNodeNotifier; } /** * Retire one node's lifecycle to `cancelled → done` and tear down its dispatch * task fire-and-forget. THE single shared implementation of the triplicated * "retire node + M10 session-slot refund + cancelTask fire-and-forget" pattern * previously copied across `cancelOne` (`cancellation.ts`), `cancelNode` * (`approval-handler.ts`), and the inline block in `cancelPendingUpstreams` * (`cascade-canceller.ts`) — so the session-refund logic cannot drift between * the three cancellation lanes. * * Steps, in order: * 1. `canTransitionNode(status, Cancelled)` guard — double-guard with the * Cancellable rule (the transition table would throw on an illegal * transition, so we guard before calling). * 2. M10 session-slot refund — when `opts.refund` is on (default), a RUNNING * node with a live dispatch task decrements the graph-level * `sessionsSpawned` counter synchronously, BEFORE the transition * (refund-before-transition ordering is load-bearing: the async * termination callback bails on the now-`done` node and would never * refund). * 3. `markCancelled` then `markDone` — the node lands terminal `done`. * 4. `removeFromFrontier` — the retired node stops being a dispatch * candidate. * 5. `cancelTask` fire-and-forget — when a port is provided and the node has * a `dispatchTaskId`; never awaited, never acked. When `opts.cancelCalls` * is present, the task id is recorded into it. * 6. Optional `onCancelled` hook — surfaces the retirement to the caller * (monitor H4) identically to signal-driven transitions. */ export declare function retireCancelledNode(state: EngineState, node: NodeRuntimeState, reason: string, opts?: RetireCancelledNodeOptions): void; /** * Cancel one or more named node ids (optionally cascading to their transitive * downstream dependents), reusing ONLY the existing lifecycle machinery. * * Policy: * - Each requested id is expanded to its effective member set (loop-group * targets pull in their full member set). Unknown ids are ignored (skipped). * - Every effective-target node in `pending | ready | running` is retired to * `cancelled → done` via {@link markCancelled} / {@link markDone}, removed * from the frontier, and — when a cancel seam is present and it carries a * `dispatchTaskId` — has its dispatch task cancelled fire-and-forget (never * awaited). * - With `cascade`, the effective targets' transitive downstream closure over * `graphDeclaration.edges` is retired the same way. * - Nodes already `completed`, `blocked`, or terminal are reported in `skipped` * and left untouched. The whole-graph `cancel()` path is never invoked. * * Pure synchronous state mutation + fire-and-forget dispatch teardown — never * awaits a cancellation acknowledgement (matches the cascade-canceller §3.3 * convention). * * @param state Engine state (source of per-node runtime state). * @param nodeIds Node ids to cancel (loop targets expand to their members). * @param options `{ cascade?: boolean }` — cascade the cancellation to * transitive downstream dependents when true. * @param dispatchPort Optional cancellation seam; when omitted, only the node * lifecycle is advanced (no dispatch task teardown). * @param onCancelled Optional per-node notification hook (monitor H4) invoked * with `(nodeId, reason)` for every node actually retired; * wired by the engine runtime to `notifyNodeTerminal`. * @returns A {@link CancelScopeReport} describing what was retired and skipped. */ export declare function cancelNodes(state: EngineState, nodeIds: string[], options?: CancelScopeOptions, dispatchPort?: CancelDispatchPort, onCancelled?: CancelNodeNotifier): CancelScopeReport; //# sourceMappingURL=cancellation.d.ts.map