/** * Every write that moves a run's lifecycle, and the executor lock each one checks. * * One module holds the lifecycle SQL — creating the run, beginning a document * execution, finishing one, publishing a status — because these rows describe * one another. A begin that inserted an execution without publishing `running`, * or a settlement that finished the record without publishing the outcome, would * leave a run whose two halves disagree, and inspection would report whichever * half it happened to read. * * ## The executor lock is checked where it is used * * Each transition validates the exact live executor lock inside its own * transaction, not before opening it. A lock checked in a caller and passed * along could have been released by the time the write lands. * * ## The bodies do not yield * * From the validation to the commit, a transition body is ordinary synchronous * code. Suspending in the middle would let the scope that owns the executor * lock tear down between "this caller may write" and the write — the transaction * would commit under a lock that is no longer held. What has to happen before * the transaction happens before it. */ import type { DatabaseSync } from "node:sqlite"; import { type Operation, type Result } from "effection"; import { type WorkflowBeginRequest, type WorkflowExecutionBegun, type WorkflowForkRequest, type WorkflowRunCreation } from "../lifecycle/execution.js"; import type { LegacyWorkflowSourceReader } from "../lifecycle/source.js"; import type { WorkflowRunDatabase } from "../storage/api.js"; import { type WorkflowRunComparison } from "../storage/compatibility.js"; import { type DocumentExecutionCompletion, type DocumentExecutionRecord, type WorkflowRunRecord } from "../storage/record.js"; import type { WorkflowRunConnections } from "./connections.js"; import type { ExecutorLockHold } from "./executor.js"; import type { ForkSourceSnapshot } from "./fork-source.js"; import { type ForkHeadEvents } from "./fork-write.js"; /** Shared with `create()`, so one statement writes an immutable run. */ export declare const INSERT_RUN = "INSERT INTO workflow_run\n (id, run_id, definition, base, props, status, created_at, updated_at)\n VALUES (1, ?, ?, ?, ?, 'running', ?, ?)"; /** * Begin one document execution under this exact executor lock. * * For a run that does not exist yet, the schema, the immutable run, its * retrieval metadata, an empty Workspace, the first execution record and * `running` are one transaction: a reader either finds a whole run or no run, * never a half-initialized one. */ export declare function beginExecution(connections: WorkflowRunConnections, path: string, hold: ExecutorLockHold, authorize: () => ExecutorLockHold, request: WorkflowBeginRequest, readLegacySource?: LegacyWorkflowSourceReader): Operation>; /** * What one begin transaction committed. * * A refusal is an outcome, not an absence: the previous workflow executor's execution was * still accounted for, and that has to survive being told this caller may not * continue. The refusal itself is translated outside the transaction. */ export type BeginOutcome = ({ readonly kind: "begun"; } & WorkflowExecutionBegun & { readonly closed?: DocumentExecutionRecord; }) | { readonly kind: "refused"; readonly reason: Error; readonly closed?: DocumentExecutionRecord; }; /** * Admit one fork and begin its first execution, in one transaction. * * The source was read before this opened, into an immutable snapshot: the * source's executor lock is never taken and no statement here touches it. What * commits is the fork's schema, its immutable run, the copied Workspace content * and roots, the restored checkpoint Workspace, the inherited journal prefix * with its provenance, the lineage and the first execution — all of it, or a * destination that holds nothing. * * A fork id already in use is compatible only when every term of the fork's * identity agrees: the source run, the checkpoint, the definition including its * component bundle and pinned commit, the base and the normalized props. When * they do, this is the same fork again and it continues like any other run. */ export declare function forkExecution(connections: WorkflowRunConnections, path: string, hold: ExecutorLockHold, authorize: () => ExecutorLockHold, request: WorkflowForkRequest, snapshot: ForkSourceSnapshot, head: ForkHeadEvents, readLegacySource?: LegacyWorkflowSourceReader): Operation>; /** * Build the whole fork somewhere nothing recognizes it as a run, and hand back * its open database. * * A compatibility replay needs the fork's own Workspace: a `` resolves * through the run's filesystem, and a replay run without one produces effects * of a different kind entirely and diverges for a reason that has nothing to do * with the candidate. So the fork is assembled in full at a staging path first, * replayed there, and thrown away — and only then, if it proved compatible, is * the same assembly committed at the run's own path. * * The staging file belongs to this operation's scope. Whatever happens, it goes * when that scope ends, and one left by an interrupted attempt is replaced * rather than continued. */ export declare function stageFork(connections: WorkflowRunConnections, path: string, request: WorkflowForkRequest, snapshot: ForkSourceSnapshot, head: ForkHeadEvents, readLegacySource?: LegacyWorkflowSourceReader): Operation>; /** * Finish this execution and publish what the run became, together. * * A status line says what was retained, so the record that says the execution * ended and the state that says what it ended as commit at once or not at all. */ export declare function settleExecution(connections: WorkflowRunConnections, path: string, hold: ExecutorLockHold, authorize: () => ExecutorLockHold, offered: DocumentExecutionCompletion): Operation>; /** The immutable terms a creation offers for the run id it names. */ export declare function creationComparison(runId: string, creation: WorkflowRunCreation): WorkflowRunComparison; /** The retrieval metadata a run holds, for a caller that already opened it. */ export declare function readRetrievalRow(database: DatabaseSync): import("../storage/record.js").DefinitionRetrieval | undefined; /** * Make one run terminal under the executor lock without starting anything. * * There is no live execution to halt here: acquiring the lock is what proved * that. What is left is retained state, and the rules are the architecture's — * a root Close means the canonical outcome already won and cancellation is * refused; an unfinished execution left by a workflow executor that went away is finished * as cancelled; a run with nothing running becomes cancelled directly; a run * already cancelled says so again; and a completed or failed run is not * something to cancel. */ export declare function cancelRun(connections: WorkflowRunConnections, path: string, hold: ExecutorLockHold, authorize: () => ExecutorLockHold): Operation>;