/** * The run record: what a workflow run IS, beside the journal that says what it DID. * * The journal on WFJ is append-only and authoritative about events. This is last-value-wins and * authoritative about state — who holds the run, what state it is in, and the PIN SET resolved once * at start. Two runs of one program under different pins are two different runs, so a resume reads * the pins back rather than re-deriving them: "the default" is a property of the interpreter, and * the interpreter is the thing that may have changed between attempts. * * It also carries the one fact the journal cannot hold about itself: **how far the journal has * got**. A record deleted from the TAIL of a run's subject leaves the survivors contiguous — the * ordinal chain sees nothing missing, because the missing part is the part it would have compared * against — and the per-subject head is recalculated backwards, so the fence accepts an append at a * head the run already moved past. Measured end to end: the run replays SHORT and intact, a * successor activates on it, and it resumes from a prefix that is missing work the run really did. * No anchor inside the journal can detect that. `journalHigh` is the anchor OUTSIDE it. * * What that anchor does and does not cover, stated plainly because a guard believed to be wider * than it is is worse than none: it is written at each ACTIVATION, so it detects truncation back * past the last takeover. Steps appended since that activation are not covered — writing the record * per append would double every step's cost, and the journal's own ordinal chain already covers * every interior loss. */ import type { KV } from "@nats-io/kv"; import { type MergedRecord } from "./endpoint-records.js"; /** * The pins, resolved. Structurally the language's `RunPins` — `@cotal-ai/core` does not depend on * `@cotal-ai/lang`, and the wire shape is the contract between them rather than a shared type. */ export interface RunPinsValue { readonly seed: string; /** The run's logical epoch. `now()` derives from THIS, never from the resuming host's clock. */ readonly startedAt: number; readonly yieldEvery: number; readonly stepBudget: number; readonly effectCeiling: number; readonly languageVersion: string; } /** * The immutable half: what this run IS, decided once and never re-decided. * * Deliberately NOT carrying a program hash yet. The interpreter reports one, but only after a run, * and a hash computed here from the source would be a different function's answer wearing the same * name — the divergence check that matters is per-step and the language already makes it from the * recorded input hashes. A source-identity pin is its own item, not a field filled with a guess. */ export interface RunSpecValue { readonly v: 1; readonly run: string; readonly pins: RunPinsValue; readonly createdAt: number; /** * Present on a fork's child: the parent run and the step key the cut excluded. Lineage is a * fact about what this run IS, decided once at the fork, so it lives in the immutable half. * Absent on a run started fresh, and legal to be absent on a child recorded before this field * existed: absence reads as "lineage unknown", never as "not a fork". */ readonly forkedFrom?: { readonly run: string; readonly step: string; }; } /** * What a run is doing now. * * `released` and `failed` are not the same thing and the distinction is the point: a program that * failed has a result and the journal has it, while a released run has no result at all — its * driver stopped holding it. Recording one as the other would write down a conclusion about work * nobody observed. */ export type RunState = "running" | "released" | "completed" | "failed"; export interface RunStatusValue { readonly v: 1; readonly observedSpecRevision: number; readonly state: RunState; readonly holder: string; readonly epoch: number; readonly fencingToken: number; /** * The highest journal ordinal this run is KNOWN to have reached, written at each activation. * A replay whose last ordinal is BELOW this has lost records from its tail. */ readonly journalHigh: number; readonly at: number; } /** A run's journal is missing records from its END — which nothing inside the journal can see. */ export declare class RunJournalTailTruncated extends Error { readonly run: string; readonly recordedHigh: number; readonly replayedHigh: number; constructor(run: string, recordedHigh: number, replayedHigh: number); } /** Read a run's record. `undefined` = this run has never been started. */ export declare function readRunRecord(kv: KV, endpoint: string, runId: string): Promise | undefined>; /** * Create a run's spec. Create-only: a spec that already exists means this run was started before, * and starting it again under a fresh set of pins would be a different run wearing the same id. */ export declare function createRunSpec(kv: KV, endpoint: string, runId: string, value: Omit): Promise; /** * Write a run's status. `expectedRevision` is `undefined` for the first write and the last read * revision thereafter — the CAS is what keeps two drivers from taking turns overwriting each * other's view of who holds the run. */ export declare function writeRunStatus(kv: KV, endpoint: string, runId: string, value: Omit, expectedRevision?: number): Promise; /** * The tail check, run BETWEEN the replay and the activation. * * Before, and not after, because an activation appended over a rolled-back head is itself a record * written into a journal we already know is wrong — and it would be written at an ordinal the * deleted records already used, which is a hole the chain would then blame on someone else. */ export declare function assertJournalTailIntact(runId: string, recorded: RunStatusValue | undefined, replayedHigh: number): void; //# sourceMappingURL=run-record.d.ts.map