// app/deliveryGraphRun.ts — the delivery-graph run AGGREGATE (ADR 0005 Decision 7). // // The cockpit dispatch action (`app/deliveryGraphDispatch.ts`, invoked from `dispatchDeliveryGraph`) // turns a staged, agent-authored `DeliveryGraph` into a RUNNING engine-native process (via the S4 // runner). Because these graphs merge PRs and publish packages, dispatch must be idempotent and // at-most-once. This module is the durable aggregate that makes that true and gives the cockpit a row // to show WHERE a run is: // // • the idempotency fence — a run is keyed by `run_key` (a caller `idempotencyKey`, else the graph's // content digest). A re-dispatch collapses onto the same row, so an in-flight run short-circuits // instead of double-launching (mirrors `plans`' `alreadyRunning`). // • the content digest — `digest` is the content-address of the compiled definition, persisted so // the cockpit and reconcilers can relate a run to the proposal it came from. // • the derived parked-node phase — `phase`/`phase_node_id` is the display-only "where is it parked" // projection `pollDeliveryGraphPhase` recomputes from engine truth (the running instance's open // user tasks), generalising the `epic_phase` derived-phase machinery to a DYNAMIC compiled process. // // The pure helpers here (`computeRunKey`, `buildHumanLabels`, `deriveDeliveryPhase`) are engine/DB-free // so they unit-test in isolation; the dispatch action and the poller supply the I/O. // // NOTE (issue #460): the `awaiting-approval` status remains a RESERVED member of the lifecycle union // (like `abandoned`) but is no longer produced — dispatch is now an operator action in the cockpit, so // there is no agent-facing approval gate to park a run at. The old replayable `approvalToken` and the // approval-park write were removed with the agent `start` door. import type { DataLayer, ProcessInstanceState } from "@nanobpm/urban"; import type { CompileDeliveryGraphResult } from "../nano-generated/api-io.d.ts"; import { isUniqueConstraintFence } from "./dbFence.ts"; import { DELIVERY_HUMAN_ELEMENT, isDeliveryHumanElement } from "./deliveryHuman.ts"; const now = () => new Date().toISOString(); /** One delivery-graph run — the durable row. `side_effecting` is a SQLite boolean (0/1). */ export interface DeliveryGraphRun { run_key: string; process_key: string | null; process_definition_id: string | null; digest: string; status: DeliveryGraphRunStatus; side_effecting: number; node_count: number; human_node_count: number; side_effect_count: number; title: string | null; phase: string | null; phase_node_id: string | null; human_labels: string | null; created_at: string; updated_at: string; /** The operator-dismissal stamp (issue #641). Set by `acknowledgeDeliveryGraph` on a TERMINAL run so * the `delivery_graph_read_model` VIEW folds its `list_bucket` to 'history'; NULL while the run is * live or terminal-but-undismissed (it stays in Active until an operator ticks it off). */ acknowledged_at: string | null; } /** The run lifecycle. `awaiting-approval` is RESERVED but no longer produced (issue #460 moved dispatch * to an operator action, so runs are only ever created at launch) — kept in the union to preserve the * durable enum. `running` = dispatched to the engine; `done`/`failed`/`abandoned` = terminal. */ export const DELIVERY_GRAPH_RUN_STATUSES = [ "awaiting-approval", "running", "done", "failed", "abandoned", ] as const; export type DeliveryGraphRunStatus = typeof DELIVERY_GRAPH_RUN_STATUSES[number]; /** The ACTIVE statuses — a run in one of these is still in flight and shows in the cockpit's active * grid (`pages/overview.page.json`'s "Active Delivery Graphs" filter). Note this is the DISPLAY set, * broader than the instanceTracking binding: only `running` is backed by a live engine instance * (non-null `process_key`), so ONLY `running` is instance-tracked (nano.app.json). A parked * `awaiting-approval` run has no instance (`process_key` NULL) — it is shown here but not reconciled * by the `process_key`-keyed reconciler. */ export const DELIVERY_GRAPH_ACTIVE_STATUSES: readonly DeliveryGraphRunStatus[] = [ "awaiting-approval", "running", ]; /** The terminal statuses — a run in one of these is done and drops out of the active grid. Mirrors * `PLAN_TERMINAL_STATUSES`; the idempotency short-circuit only fires for a NON-terminal run. */ export const DELIVERY_GRAPH_TERMINAL_STATUSES: readonly DeliveryGraphRunStatus[] = [ "done", "failed", "abandoned", ]; /** The display phases for a run's derived projection. `Parked on human node: