fyflow-orchestration 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,11 @@ export interface TaskDetailLike {
6
6
  result?: unknown;
7
7
  error?: string;
8
8
  attempts?: number;
9
+ /**
10
+ * The scheduler's 1-based number of the attempt, read on `task.running` ONLY — `RN-R40`, W186, by the driver while its
11
+ * listener runs. Never on a record: the record's `attempts` is the scheduler's count (`OR-R4`, P36 (i)).
12
+ */
13
+ attemptNumber?: number;
9
14
  timestamp?: number;
10
15
  startTime?: number;
11
16
  endTime?: number;
@@ -15,7 +20,7 @@ export interface TaskDetailLike {
15
20
  * `OR-R6`'s refusal. **It sets its own `name`**, as `BoundaryRefused` and `ResumeRefused` do, so the
16
21
  * error's own render — `String(e)`, a stack's first line — reads `UncloneableResult: <message>`. Since
17
22
  * W133 the MESSAGE no longer carries the class name (Q63), and without this the error itself would
18
- * say `Error:` everywhere but `RunFailure.kind` and `stoppedBy`.
23
+ * say `Error:` everywhere but `RunFailure.kind` and `haltedBy`.
19
24
  */
20
25
  export declare class UncloneableResult extends Error {
21
26
  constructor(message?: string, options?: ErrorOptions);
@@ -29,6 +34,7 @@ export declare class UncloneableResult extends Error {
29
34
  * done task.completed -> 1 record
30
35
  * failed (optional task) task.failed -> 1 record
31
36
  * user_action task.failed, task.user_action -> 1 record, on the SECOND
37
+ * cancelled task.cancelled -> 1 record (ADR 0094)
32
38
  *
33
39
  * The third row is why this exists. `task.failed` fires for a task that did not
34
40
  * fail, its detail already reads `state: 'user_action'`, and a driver keying on
@@ -15,7 +15,12 @@ export interface OutcomeRecord {
15
15
  result: CloneableValue | null;
16
16
  error: string | null;
17
17
  attempts: number;
18
- schedulerTimestamp: number;
18
+ /**
19
+ * The scheduler's stamp — and **`null` for an ASK's effective outcome** (`spec/outcome-record.md` v0.18, P35 (e),
20
+ * [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md)): no
21
+ * scheduler was involved. A SETTLEMENT always carries a number. **BREAKING in 0.8.0** for a consumer typed on `number`.
22
+ */
23
+ schedulerTimestamp: number | null;
19
24
  observedAt: number;
20
25
  startTime: number | null;
21
26
  endTime: number | null;
@@ -39,8 +44,14 @@ export type LogRecord = OutcomeRecord | ResolutionRecord;
39
44
  * second resolution refusable.
40
45
  */
41
46
  export declare function lastRecordFor(log: readonly LogRecord[], taskId: string): LogRecord | undefined;
42
- /** `RS-R1` — a task is suspended when its last record is an outcome in `user_action`. */
43
- export declare function isSuspended(log: readonly LogRecord[], taskId: string): boolean;
47
+ /**
48
+ * `RS-R1` — a task is suspended when its last record is an outcome in `user_action`; **or, since v0.18, when it was
49
+ * ASKED and has no record at all** (P35 (b), ADR 0109): an ask parks with an `asked` fold input — not a log record,
50
+ * since section 1 fixes the log at two kinds — and the person's answer is the first record its id ever gets, so a
51
+ * second answer is refused as a second resolution is. `asked` is the caller's: whether the fold-input sequence holds an
52
+ * `asked` element for the id, which this function cannot see.
53
+ */
54
+ export declare function isSuspended(log: readonly LogRecord[], taskId: string, asked?: boolean): boolean;
44
55
  /**
45
56
  * `spec/outcome-record.md` section 6 — the record ROUTING reads, which is never
46
57
  * the log directly.
@@ -52,8 +63,12 @@ export declare function isSuspended(log: readonly LogRecord[], taskId: string):
52
63
  * `OR-R8`: derived, never appended. Appending it would make the log contain its
53
64
  * own conclusions, and a second fold would derive a third record from the second.
54
65
  *
66
+ * **An ASK's** — a resolution with NO settlement before it, which `RS-R1` admits only for an asked task — is DERIVED
67
+ * from the resolution alone (`spec/outcome-record.md` section 7, v0.18, P35 (e), ADR 0109): the answer is the node's
68
+ * result, `attempts` `0` and every scheduler field `null`, because no dispatch happened for those fields to describe.
69
+ *
55
70
  * @returns `null` when the task has not settled, or has settled `user_action` and
56
- * has no resolution — a suspended node has no effective outcome, which is
57
- * exactly right: it has not finished and nothing routes.
71
+ * has no resolution, or was asked and has no answer — a suspended node has no
72
+ * effective outcome, which is exactly right: it has not finished and nothing routes.
58
73
  */
59
74
  export declare function effectiveOutcome(log: readonly LogRecord[], taskId: string): OutcomeRecord | null;
@@ -21,12 +21,16 @@ import type { OutcomeRecord } from './log.js';
21
21
  export interface DecisionContext {
22
22
  /**
23
23
  * `OR-R9` — ALWAYS the node's effective outcome, so a predicate sees one shape
24
- * whether the node settled or was resolved, and `terminalState` is never undefined.
24
+ * whether the node settled or was resolved, and `terminalState` is never undefined. **For an ASK it is derived from
25
+ * the person's answer** (`spec/outcome-record.md` section 7, v0.18): `result` the answer, `attempts` `0`, and
26
+ * `schedulerTimestamp` and the timing fields `null` — no scheduler was involved.
25
27
  */
26
28
  outcome: OutcomeRecord;
27
29
  /**
28
30
  * **What this EDGE declared it needs** — the object its `needs` describes, resolved
29
- * against the run's own outputs exactly as a node's is (`FD-C1`, `FD-C2`).
31
+ * against the run's own outputs exactly as a node's is (`FD-C1`, `FD-C2`) — and, since
32
+ * v0.44, against the value the RUN entered with, for a `"$payload"` key (`FD-C3`,
33
+ * ADR 0117): so a predicate routes on `needs.run.amount` with no task before it.
30
34
  *
31
35
  * **`{}` when the edge declares none, never `undefined`**, for rule 4's own reason
32
36
  * one level out: *a predicate written `ctx.needs.flag` must mean one thing*, and a
@@ -58,9 +62,10 @@ export declare function eligibleEdges(node: FlowNode, terminalState: TerminalSta
58
62
  * **The ONE edge a gate takes for a route** — `spec/flow-definition.md` section 2's gate,
59
63
  * [ADR 0066](../../doc/decisions/0066-a-gate-is-a-decision-with-memory-that-names-a-registered-type-and-wires-every-route-exactly-once.md)
60
64
  * rule 4, [W93](../../doc/work-items/w93-what-a-gate-does-in-a-run.md). An edge carries the route it names,
61
- * and **an edge with no `route` carries the type's DEFAULT route** — never every route, and not the first.
62
- * Pure, as `selectEdge` is: the caller has already held `route` to the type's declared routes (`FD-R5`), and
63
- * `FD-V41` wires every declared route by exactly one edge, so this is total over what reaches it — `null` is
65
+ * and **an edge with no `route` carries the gate's DEFAULT route** — never every route, and not the first.
66
+ * `defaultRoute` is the GATE's resolved set's (`routesOf`, ADR 0092 rule 3), never read off the type here.
67
+ * Pure, as `selectEdge` is: the caller has already held `route` to the gate's resolved routes (`FD-R5`), and
68
+ * `FD-V41` wires every route of that set by exactly one edge, so this is total over what reaches it — `null` is
64
69
  * the assertion that `FD-V41` held, and the driver stops the run on it rather than routing nowhere.
65
70
  */
66
71
  export declare function gateEdge(node: GateNode, defaultRoute: string, route: string): Edge | null;
@@ -0,0 +1,94 @@
1
+ import type { FoldInput, NodeRun, RunOutcome, RunState } from './run-state.js';
2
+ /**
3
+ * **A count of the work an accumulator did** — P39 decision 6 and stop-and-ask (e): one per input applied, and one per
4
+ * entry, element or cell it reads or writes while loading, copying or materialising. Clock-free, so a ratio of two
5
+ * counts is a property of the code and not of a machine (Q179). Owned by the caller; absent, nothing is counted.
6
+ */
7
+ export interface FoldCounter {
8
+ visits: number;
9
+ }
10
+ export declare class Accumulator {
11
+ private runId;
12
+ private outcome;
13
+ private haltedBy;
14
+ private cancelRequest;
15
+ private interruption;
16
+ private paused;
17
+ /** Entries written since the load, by task id, in first-write order. A task id not here reads through to the base. */
18
+ private readonly nodes;
19
+ /** Instances touched since the load, by opener and then by the opener's branch path, in first-touch order. */
20
+ private readonly regions;
21
+ private readonly pathEnds;
22
+ private readonly resumes;
23
+ private readonly gated;
24
+ private counts;
25
+ private readonly base;
26
+ private readonly counter;
27
+ private constructor();
28
+ /** An accumulator over `state`, which it reads and never mutates. `counter`, when given, is charged for its work. */
29
+ static load(state: RunState, counter?: FoldCounter): Accumulator;
30
+ private tick;
31
+ /**
32
+ * **The outcome as the last applied input left it** — what {@link materialise} would write into `outcome`, read in
33
+ * O(1). The runtime's `RunHandle.outcome()` and `done`'s predicate read this, never a materialisation (P39 prediction 4).
34
+ */
35
+ outcomeNow(): RunOutcome;
36
+ /**
37
+ * **One task's entry, or `undefined`** — the object {@link materialise} would place at `nodes[taskId]`, read in O(1).
38
+ * An OWN entry only: a task id naming an `Object.prototype` member (`toString`, `__proto__`) is a task the run never
39
+ * dispatched, where `prev.nodes[taskId]` on a plain record would read the inherited member.
40
+ */
41
+ entry(taskId: string): NodeRun | undefined;
42
+ /** `prev.nodes[taskId]` — the entry written since the load, else the base's own read, prototype and all. */
43
+ private node;
44
+ /** `prev.regions[regionId]?.[regionPath]` — the touched instance, else the base's. */
45
+ private instance;
46
+ /** The counts, taken from everything the state holds on first need — O(the state), once per accumulator. */
47
+ private tally;
48
+ private countNode;
49
+ private stateCount;
50
+ /** `nodes[taskId] = entry`, the counts kept. An entry is REPLACED, never mutated: a materialised state may hold the old one. */
51
+ private setNode;
52
+ /**
53
+ * The instance at (`regionId`, `regionPath`) to write to — the touched one, else a COPY of the base's (O(its branch
54
+ * paths), once), else an empty one; with its share of the region counts taken out, for {@link settleRegion} to put back.
55
+ */
56
+ private touchRegion;
57
+ private unweigh;
58
+ /** The instance's share of the region counts put back after a write. */
59
+ private settleRegion;
60
+ private addPathEnd;
61
+ /**
62
+ * **Apply one input.** `true` when the arm moved the state — where `step` before W194 returned a NEW object — and
63
+ * `false` where it returned the state it was handed, so `step` can return that same object. Throws where
64
+ * that `step` threw: a malformed task id (`parseTaskId`), a kind it does not name (`input.record` undefined) — within
65
+ * the module header's stated limit on region paths.
66
+ */
67
+ apply(input: FoldInput): boolean;
68
+ private applyRegion;
69
+ /**
70
+ * **Section 3's precedence, read off the counts** — `RN-R7`, `RN-R8`, `RN-R10`, `RN-R28`, `RN-R30` and `RN-R32` in the
71
+ * one order they have always had. Called by exactly the arms that recomputed before W194. Always `true`: an arm that
72
+ * recomputes has moved the state.
73
+ */
74
+ private withOutcome;
75
+ private precedence;
76
+ /**
77
+ * **One plain `RunState`**, built from the base and what was written since the load: a record or list nothing wrote is
78
+ * the base's own object, and one that was written is a new one with the base's entries in their places and the new
79
+ * entries after them, in first-write order — the key order a spread-per-step produced. O(what it builds).
80
+ */
81
+ materialise(): RunState;
82
+ private list;
83
+ private materialiseRegion;
84
+ }
85
+ /**
86
+ * **The fold, with its work counted** — the INTERNAL entry point P39 decision 6 names: `fold` is this with no counter.
87
+ * One accumulator, every input applied in append order (`RN-R3`), materialised once: linear in `inputs`.
88
+ */
89
+ export declare function foldWith(inputs: readonly FoldInput[], counter?: FoldCounter): RunState;
90
+ /**
91
+ * **One step, with its work counted** — `step` is this with no counter: load, apply, materialise, and the state it was
92
+ * handed itself back wherever the arm moves nothing. O(the state it is handed).
93
+ */
94
+ export declare function stepWith(prev: RunState, input: FoldInput, counter?: FoldCounter): RunState;
@@ -91,11 +91,10 @@ export interface ArrivalCountState {
91
91
  * That rule's own section says *"`arrival-count` is immune, because its `N` comes
92
92
  * from `region.branches.length` at `open`"* — true until this change and false
93
93
  * after it. Every `branch.opened` for a region precedes any other event for it, so
94
- * `need` is final before any arrival can be counted against it. The clause remains
95
- * unfalsifiable through a conformant driver — `FD-V25` makes a map's branch a
96
- * `task` and entering a task dispatches without emitting, which is `FD-V9`'s role
97
- * for a fanout played by a second rule — but what RESTS on it is now both counting
98
- * policies rather than one.
94
+ * `need` is final before any arrival can be counted against it. *(This read "the clause remains unfalsifiable through
95
+ * a conformant driver" until W180: since `spec/flow-definition.md` v0.42 a branch entry or a map's branch may be an
96
+ * `ask`, which EMITS on entry, so the clause's guarantor is the driver's two-pass emission — `spec/join-policy.md`
97
+ * v0.23.)* What RESTS on it is now both counting policies rather than one.
99
98
  */
100
99
  export declare const arrivalCount: JoinPolicy<ArrivalCountState>;
101
100
  export interface LiveTokenState {
@@ -6,18 +6,29 @@
6
6
  * ladder L3 checks by folding the same log twice and deep-comparing, and it is
7
7
  * what makes [Q14](../../doc/questions/q14.md)'s answer an answer rather than a
8
8
  * restated leaning.
9
+ *
10
+ * **Since v0.29 the arms live in ONE place, `src/fold/accumulator.ts`** (`RN-R43`,
11
+ * [ADR 0120](../../doc/decisions/0120-the-fold-is-one-accumulator-and-step-stays-pure.md)): an accumulator mutated in
12
+ * place WITHIN one call and materialised into a plain state, so nothing survives a call and `RN-R1` holds as before.
13
+ * This module keeps the types, `emptyState`, `isTerminal`, `nodeOf` and `foldLog`, and `fold` and `step` call into it.
14
+ * That module is internal: `index.ts` re-exports this one and never it.
9
15
  */
10
16
  import type { LogRecord, OutcomeRecord } from '../driver/log.js';
11
17
  import type { RegionEvent } from '../driver/events.js';
12
18
  /**
13
- * `spec/run-state.md` section 4. Four, not five.
19
+ * `spec/run-state.md` section 4. Five, not the scheduler's six.
14
20
  *
15
21
  * `user_action` is `suspended` here — it is not terminal at flow level. And there
16
22
  * is **no `pending`**: `OR-R4` says `task.running` writes no record, so the only
17
23
  * signal the fold has is the dispatch itself, and a task is `running` from that
18
- * moment. A state nothing can produce is not a state.
24
+ * moment. A state nothing can produce is not a state. *(Since v0.26 `task.running` produces a FOLD INPUT, `attempted`
25
+ * (`RN-R40`, W186) — still no record, and it moves no state: a task is `running` from its dispatch either way.)*
26
+ *
27
+ * **`cancelled` is W158's** ([ADR 0094](../../doc/decisions/0094-a-cancelled-task-is-a-terminal-state-that-routes-no-edge-and-ends-its-run-cancelled.md)),
28
+ * copied verbatim from the settlement as `done` and `failed` are. **Widening this union is BREAKING for a
29
+ * consumer switching on it exhaustively** — `resumed`'s and `gated`'s precedent.
19
30
  */
20
- export type NodeState = 'running' | 'done' | 'failed' | 'suspended';
31
+ export type NodeState = 'running' | 'done' | 'failed' | 'suspended' | 'cancelled';
21
32
  /**
22
33
  * `spec/run-state.md` section 3. Closed, and ours — an author labels why a path
23
34
  * ended, not what the run is.
@@ -26,17 +37,93 @@ export type NodeState = 'running' | 'done' | 'failed' | 'suspended';
26
37
  * [ADR 0015](../../doc/decisions/0015-a-run-whose-join-can-no-longer-fire-is-stuck.md):
27
38
  * nothing is live, nothing waits on a person, and a region's join has not fired,
28
39
  * so nothing will ever move again. v0.2 called that run **`completed`**.
40
+ *
41
+ * **`cancelled` is v0.15's seventh**, added by W158 under
42
+ * [ADR 0094](../../doc/decisions/0094-a-cancelled-task-is-a-terminal-state-that-routes-no-edge-and-ends-its-run-cancelled.md):
43
+ * a task of the run settled `cancelled`, so the driver halted it (`RN-R28`). **Widening this union is BREAKING
44
+ * for a consumer switching on it exhaustively** — `resumed`'s and `gated`'s precedent; a consumer comparing
45
+ * strings sees a value it never named, and `isTerminal` already calls it terminal.
46
+ *
47
+ * **`halted` is v0.16's NAME for the outcome 0.7.0 called `stopped`** — W171,
48
+ * [ADR 0102](../../doc/decisions/0102-a-run-the-driver-halts-is-halted-and-a-stored-stopped-reads-as-one.md), P34
49
+ * decision 2 and stop-and-ask (f): a refusal terminated the run (`OR-R10`, `RN-R8`). *Stop* is P34's resumable stop
50
+ * of a run (W174), so the old name leaves this union ENTIRELY: **BREAKING**, and loudly — a consumer's
51
+ * `outcome === 'stopped'` no longer compiles, rather than silently changing what it means. The count does not move.
52
+ *
53
+ * **`cancelling` is v0.18's EIGHTH**, added by W173 under
54
+ * [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md): the
55
+ * run's OWNER cancelled it (`driver.cancel`, a `cancelRequested` element) and a task is still stopping (`RN-R30`).
56
+ * **NOT terminal** — the third outcome {@link isTerminal} calls unfinished — so a runtime's `done` waits for the last
57
+ * task to stop. **BREAKING in 0.8.0** (P34 decision 10): a consumer switching on it exhaustively, or keying a
58
+ * `Record<RunOutcome, …>`, fails to compile; one comparing strings sees a value it never named.
59
+ *
60
+ * **`interrupted` is v0.19's NINTH**, added by W174 under
61
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md): the
62
+ * run's OWNER stopped it to resume it elsewhere (`driver.stop`, an `interrupted` element), and no `resumed` element has
63
+ * followed (`RN-R32`). **Terminal** — {@link isTerminal} needs no change, and a runtime's `done` settles on it once the
64
+ * stop's cancels have resolved — **and the one terminal outcome a resume ACCEPTS**. **BREAKING in 0.8.0** (P34 decision
65
+ * 10), as `cancelling` is.
66
+ */
67
+ export type RunOutcome = 'running' | 'completed' | 'failed' | 'awaiting_action' | 'halted' | 'stuck' | 'cancelled' | 'cancelling' | 'interrupted';
68
+ /**
69
+ * **Who cancelled the run, and why** — `RunState.cancelRequest`, `RN-R30`, v0.18. What the `cancelRequested` element
70
+ * carried: `actor` as the caller gave it, `reason` the caller's or `null` when it gave none.
71
+ */
72
+ export interface CancelRequest {
73
+ actor: string;
74
+ reason: string | null;
75
+ }
76
+ /**
77
+ * **Who stopped the run, and why** — `RunState.interruption`, `RN-R32`, v0.19. What the `interrupted` element carried:
78
+ * `actor` as the caller gave it, `reason` the caller's or `null` when it gave none.
29
79
  */
30
- export type RunOutcome = 'running' | 'completed' | 'failed' | 'awaiting_action' | 'stopped' | 'stuck';
80
+ export interface Interruption {
81
+ actor: string;
82
+ reason: string | null;
83
+ }
31
84
  /** How a branch left its region. `null` while it is still live. */
32
85
  export type BranchSettlement = 'arrived' | 'exited' | 'lost';
86
+ /**
87
+ * **One task's entry in `RunState.nodes`** — `RN-R5`, and since v0.27 its ATTRIBUTION as fields (`RN-R41`,
88
+ * [ADR 0114](../../doc/decisions/0114-a-tasks-attribution-is-fields-and-its-messages-and-progress-are-passed-through.md), P36
89
+ * stop-and-ask (j)): the run, the flow-level attempt and the scheduler's attempt number, so a reader holding an entry on
90
+ * its own needs no parse of its key. **`runId`, `flowAttempt` and `attemptNumber` are REQUIRED, and that is BREAKING in
91
+ * 0.8.0 for code that builds a `NodeRun` literal** (P36 (p)); a reader is unaffected. Fold OUTPUT only: no fold input
92
+ * carries a field derived from the task id (`spec/outcome-record.md` section 2), and none moves.
93
+ */
33
94
  export interface NodeRun {
95
+ /**
96
+ * **The run the task belongs to** — v0.27, `RN-R41`: the task id's first segment (`spec/task-id.md` section 1), equal to
97
+ * `RunState.runId` on every entry of a run's fold. On the entry so it travels with it.
98
+ */
99
+ runId: string;
34
100
  nodeId: string;
35
101
  /** `'-'` when the node is in no fan-out region. */
36
102
  branchPath: string;
103
+ /**
104
+ * **The FLOW-LEVEL attempt** — v0.27, `RN-R41`: the task id's fourth segment, `0` on every id this library mints today
105
+ * ([Q16](../../doc/questions/q16.md)'s flow half mints nothing above it). NOT the scheduler's attempt number: that is
106
+ * {@link NodeRun.attemptNumber}, named apart so the two cannot be confused (P36 (j)).
107
+ */
108
+ flowAttempt: number;
109
+ /**
110
+ * **The SCHEDULER's attempt number** — v0.27, `RN-R41`: the 1-based `attemptNumber` the LATEST `attempted` element for this
111
+ * task carried (`RN-R40`), so a retried task reads the attempt it is on, or settled on. **`null`** before its first
112
+ * attempt starts, on an ASK (never handed to a scheduler), and on every entry of a log written before 0.8.0, which holds
113
+ * no `attempted` element — never derived from a settlement's `attempts`.
114
+ */
115
+ attemptNumber: number | null;
37
116
  state: NodeState;
38
117
  result: unknown;
39
118
  error: string | null;
119
+ /**
120
+ * **What REACHED an ask** — `RN-R36`, v0.22, P35 (f),
121
+ * [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md): the value
122
+ * its `needs` composed (or the edge's value without one), as its `asked` element carried it, and KEPT after the answer.
123
+ * **Present only on an ask's node**, so every other node's entry has the five fields it always had — OPTIONAL, and
124
+ * additive: a consumer building a `NodeRun` literal is unaffected.
125
+ */
126
+ input?: unknown;
40
127
  }
41
128
  export interface RegionRun {
42
129
  /** Branch paths opened, in the order the driver emitted them (`JP-R5`). */
@@ -95,8 +182,37 @@ export interface RunState {
95
182
  reason: 'end' | 'no-eligible-edge';
96
183
  at: string;
97
184
  }[];
98
- /** Set when a refusal terminated the run (`OR-R10`, `RN-R8`). */
99
- stoppedBy: string | null;
185
+ /**
186
+ * Set when a refusal HALTED the run (`OR-R10`, `RN-R8`): the refusal's class, `: `, and its message. *(0.7.0's
187
+ * `stoppedBy` — renamed with the outcome by ADR 0102; BREAKING, and a consumer reading the old name fails to compile.)*
188
+ */
189
+ haltedBy: string | null;
190
+ /**
191
+ * **Set when the run's OWNER cancelled it** — `RN-R30`, v0.18, W173,
192
+ * [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md): the
193
+ * FIRST `cancelRequested` element's `{ actor, reason }`, and `null` for a run nobody cancelled — including one a cancel
194
+ * the scheduler made on its own ended (`RN-R28`), which carries no actor. **A new cell, BREAKING in 0.8.0 for a
195
+ * consumer that builds a `RunState` literal** (P34 decision 10).
196
+ */
197
+ cancelRequest: CancelRequest | null;
198
+ /**
199
+ * **Set while the run's OWNER has STOPPED it** — `RN-R32`, v0.19, W174,
200
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md): the
201
+ * `interrupted` element's `{ actor, reason }`, and `null` again once a `resumed` element follows it. **CURRENT, not
202
+ * cumulative** — `regions[*].suspended`'s precedent (`RN-R6`): that a resumed run was ever stopped is in the input
203
+ * sequence, not here. **A new cell, BREAKING in 0.8.0 for a consumer that builds a `RunState` literal** (P34 decision 10).
204
+ */
205
+ interruption: Interruption | null;
206
+ /**
207
+ * **Whether the run's OWNER has paused it NOW** — `RN-R34`, v0.20, W175,
208
+ * [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md): `true`
209
+ * from a `paused` element until an `unpaused` one. **A flag, NOT an outcome** (P34 (e)): a paused run still routes, and
210
+ * each dispatch it holds is `dispatched` and so `running` in `nodes` — so `outcome` is exactly what it would be without
211
+ * the pause, and no precedence moves. **CURRENT, not cumulative**: that a run was ever paused is in the input sequence.
212
+ * Neither a stop, a cancel nor a resume lifts it — only an `unpaused` element does, so a run stopped while paused
213
+ * resumes paused. **A new cell, BREAKING in 0.8.0 for a consumer that builds a `RunState` literal** (P34 decision 10).
214
+ */
215
+ paused: boolean;
100
216
  /**
101
217
  * **Every resume this run has had, in order, each the task ids it handed a scheduler AGAIN** —
102
218
  * `RN-R22`, [ADR 0065](../../doc/decisions/0065-a-resume-replays-the-stored-stimuli-refuses-what-it-cannot-regenerate-and-re-dispatches-what-was-in-flight.md),
@@ -167,7 +283,7 @@ export interface GateDecision {
167
283
  *
168
284
  * Neither is a log record, and neither belongs in one: `spec/outcome-record.md`
169
285
  * section 1 fixes the log at two kinds. This is the fold's input sequence, which
170
- * already carried region events and a stop for the same reason.
286
+ * already carried region events and a halt for the same reason.
171
287
  *
172
288
  * **`joinSettled` is W5's, and it is the third fact of this shape**
173
289
  * ([ADR 0015](../../doc/decisions/0015-a-run-whose-join-can-no-longer-fire-is-stuck.md)):
@@ -221,6 +337,25 @@ export type FoldInput = {
221
337
  } | {
222
338
  kind: 'dispatched';
223
339
  taskId: string;
340
+ }
341
+ /**
342
+ * **A branch reached an ASK, which parked** — `RN-R36`, `RN-R37`, v0.22,
343
+ * [W180](../../doc/work-items/w180-asking-and-answering-in-a-run.md),
344
+ * [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md), P35
345
+ * stop-and-ask (b): appended by the driver where a branch ENTERS an ask, with NOTHING handed to the scheduler — `taskId`
346
+ * the ask's own (`spec/task-id.md`'s scheme, attempt `0`) and `input` what reached it, composed through its `needs`
347
+ * (P35 (f)). The fold reads the node `suspended` from here, its `input` kept, until the person's `resolution`.
348
+ *
349
+ * **A structural fact, not a terminal state** (P35 decision 5, fact 13): `dispatched`'s precedent — no outcome record is
350
+ * ever written for an ask, and its only terminal fact is the person's answer. **REGENERATED on resume, never FED**
351
+ * (`RN-R19`): the replay re-enters the ask and compares the input it composes with the stored one (`RN-R20`). **The
352
+ * first kind whose payload is a value of ANY shape**, so it is deep-copied on the way out (`freezeInput`, `RN-R17`).
353
+ * **A new kind, BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P35 decision 8).
354
+ */
355
+ | {
356
+ kind: 'asked';
357
+ taskId: string;
358
+ input: unknown;
224
359
  } | {
225
360
  kind: 'pathEnded';
226
361
  reason: 'end' | 'no-eligible-edge';
@@ -231,10 +366,77 @@ export type FoldInput = {
231
366
  regionPath: string;
232
367
  joinId: string;
233
368
  fired: boolean;
234
- } | {
235
- kind: 'stopped';
369
+ }
370
+ /**
371
+ * **A refusal HALTED the run** — `OR-R10`, `RN-R8`; appended by the driver's `failRun`, `reason` the refusal's class,
372
+ * `: ` and its message. **0.7.0 wrote this element as `{ kind: 'stopped', reason }`** and ADR 0102 renamed it
373
+ * (W171, P34 stop-and-ask (f)). **The old kind is READ, never written, and is not a member of this union**: a stored
374
+ * sequence is data from before the rename, so {@link step} folds an element whose `kind` is `'stopped'` exactly as
375
+ * this one and `resumeRefusal` refuses it `halted` at its position — `CHECKPOINT_FORMAT` stays `1`, and a 0.7.0
376
+ * checkpoint of a halted run is refused for the right reason while one of a live run still resumes. Nothing in this
377
+ * library writes `'stopped'` again, and the type says so: a consumer's `kind === 'stopped'` no longer compiles.
378
+ * **BREAKING in 0.8.0** (P34 decision 10): a consumer that switched on, compared or CONSTRUCTED the old kind fails
379
+ * to compile.
380
+ */
381
+ | {
382
+ kind: 'halted';
236
383
  reason: string;
237
384
  }
385
+ /**
386
+ * **The run's OWNER cancelled it** — `RN-R30`, `RN-R31`, v0.18,
387
+ * [W173](../../doc/work-items/w173-cancelling-a-run.md),
388
+ * [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md), P34
389
+ * decision 4: appended ONCE by `Driver.cancel`, after the hold and BEFORE the scheduler is asked to cancel anything,
390
+ * so every `task.cancelled` it causes folds under it. `reason` is `null` when the caller gave none. The fold reads the
391
+ * run `cancelling` from here while a dispatched task is unsettled, and `cancelled` once none is.
392
+ *
393
+ * **Never fed to a replay**: a resume refuses a stored one `cancelled` before replaying anything (`RN-R23`). **A new
394
+ * kind, BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P34 decision 10).
395
+ */
396
+ | {
397
+ kind: 'cancelRequested';
398
+ actor: string;
399
+ reason: string | null;
400
+ }
401
+ /**
402
+ * **The run's OWNER stopped it, to resume it elsewhere** — `RN-R32`, `RN-R33`, v0.19,
403
+ * [W174](../../doc/work-items/w174-stopping-a-run-to-resume-it-elsewhere.md),
404
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md), P34
405
+ * decision 3 and stop-and-ask (d): appended ONCE by `Driver.stop`, after the hold and the driver going DEAF and BEFORE
406
+ * it asks the scheduler to cancel anything, so no `task.cancelled` the stop causes — nor any settlement at all — follows it. `reason` is `null` when the
407
+ * caller gave none. The fold reads the run `interrupted` from here until a `resumed` element.
408
+ *
409
+ * **FED to a replay** (`RN-R19`), at its own position — the one terminal element a resume does not refuse. **A new
410
+ * kind, BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P34 decision 10).
411
+ */
412
+ | {
413
+ kind: 'interrupted';
414
+ actor: string;
415
+ reason: string | null;
416
+ }
417
+ /**
418
+ * **The run's OWNER paused it** — `RN-R34`, `RN-R35`, v0.20,
419
+ * [W175](../../doc/work-items/w175-pausing-a-run.md),
420
+ * [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md), P34
421
+ * stop-and-ask (e): appended ONCE by `Driver.pause`, at rest — between two of the driver's steps — and from it on every
422
+ * dispatch is appended and HELD. The fold sets `RunState.paused`; no outcome moves.
423
+ *
424
+ * **FED to a replay** (`RN-R19`), at its own position, so a run stopped while paused resumes paused. **A new kind,
425
+ * BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P34 decision 10).
426
+ */
427
+ | {
428
+ kind: 'paused';
429
+ actor: string;
430
+ }
431
+ /**
432
+ * **The run's OWNER unpaused it** — `RN-R34`, `RN-R35`, v0.20, W175, ADR 0106: appended ONCE by `Driver.unpause`, at
433
+ * rest, BEFORE the held tasks are handed on, in dispatch order. The fold clears `RunState.paused`. **FED to a replay**;
434
+ * **a new kind, BREAKING in 0.8.0** (P34 decision 10).
435
+ */
436
+ | {
437
+ kind: 'unpaused';
438
+ actor: string;
439
+ }
238
440
  /**
239
441
  * **The run was RESUMED, and these are the tasks handed to a scheduler again** — `RN-R22`,
240
442
  * [W89](../../doc/work-items/w89-what-resuming-is.md), P23 stop-and-ask (a), answered: *a resume
@@ -269,17 +471,61 @@ export type FoldInput = {
269
471
  */
270
472
  | ({
271
473
  kind: 'gated';
272
- } & GateDecision);
474
+ } & GateDecision)
475
+ /**
476
+ * **An ATTEMPT of a task started** — `RN-R40`, v0.26,
477
+ * [W186](../../doc/work-items/w186-attempts-recorded-and-carried-across-a-resume.md),
478
+ * [ADR 0113](../../doc/decisions/0113-a-tasks-attempts-are-recorded-and-carried-across-a-resume.md), P36 stop-and-ask (f):
479
+ * appended by the driver for the scheduler's `task.running` of a task THIS run dispatched, carrying the scheduler's
480
+ * 1-based `attemptNumber` — and ONLY when that number EXCEEDS the last one recorded for the task, so a requeue (which
481
+ * repeats its number) appends nothing. For every task, with or without a `retryPolicy`. **Appended at REST**: a
482
+ * `task.running` heard on the driver's own step — an inline pool fires it INSIDE `addTask` — is appended when that step
483
+ * ends, so the element always lies BETWEEN two steps, where a resume's replay feeds it (`RN-R19`).
484
+ *
485
+ * **FED to a replay** (`RN-R19`'s seventh kind): only the scheduler can produce it, so the replay cannot regenerate it;
486
+ * the highest number the replay was fed for a task is what a resume re-hands it with as `attemptsSpent` (`RN-R21`), so a
487
+ * task's retry budget is NOT reset by a crash (Q365). **Moved nothing in `RunState` at v0.26; since v0.27 it sets ONE
488
+ * cell, its task's `NodeRun.attemptNumber`** (`RN-R41`, W187) — the run's outcome, every state and every other cell are
489
+ * what they would be without it. **Never a log record**
490
+ * (`OR-R4`: `task.running` writes none) — `cancelRequested`'s precedent. **A new kind, BREAKING in 0.8.0 for a consumer
491
+ * switching on `kind` exhaustively** (P36 decision 8, (p)).
492
+ */
493
+ | {
494
+ kind: 'attempted';
495
+ taskId: string;
496
+ attemptNumber: number;
497
+ };
273
498
  export declare function emptyState(): RunState;
274
499
  /**
275
- * One step. **Returns a new state; never mutates the one it was given** — which is
276
- * what lets `RN-R2` hold (folding a prefix then one more record equals folding the
500
+ * One step. **Never mutates the state it was given, and returns a NEW one wherever the input moves something** — and
501
+ * that state ITSELF where it moves nothing (below) — which is what lets `RN-R2` hold (folding a prefix then one more record equals folding the
277
502
  * whole) and what makes a fold usable while a run is still going.
503
+ *
504
+ * **Its work is O(the state it is handed), not O(the input)** — `RN-R43`, v0.29,
505
+ * [ADR 0120](../../doc/decisions/0120-the-fold-is-one-accumulator-and-step-stays-pure.md): it loads `prev` into the
506
+ * fold's one accumulator, applies the input and materialises a new plain state, and a pure function from one plain
507
+ * `RunState` to another cannot do less than copy the record it changes. So stepping every input of a long run costs
508
+ * O(run) per input; {@link fold} over the whole sequence is linear. A consumer that needs a run's state after every input
509
+ * reads it where a runtime keeps it, rather than stepping a copy of its own.
510
+ *
511
+ * **Each arm behaves on ANY state it is handed exactly as it did before v0.29** — not only on one a fold can reach. The
512
+ * outcome is recomputed only by the arms that recomputed it, so a hand-built state whose `outcome` disagrees with its
513
+ * cells keeps that outcome across `paused`, `unpaused`, `gated`, `attempted` and a `resumed` that ends no stop; and an
514
+ * input that moves nothing — a second cancel request, or one after the run is already `cancelled`, a second stop or
515
+ * pause, an unpause of a run not paused, a dispatch or an ask of a
516
+ * task already in `nodes`, a region instance already entered, an attempt of a task not in `nodes` — hands back `prev`
517
+ * ITSELF. A cell no input touched is `prev`'s own object in the result. (Its one stated limit, a region path naming an
518
+ * `Object.prototype` member, which no driver appends, is `src/fold/accumulator.ts`'s header's.)
278
519
  */
279
520
  export declare function step(prev: RunState, input: FoldInput): RunState;
280
521
  /**
281
522
  * The fold. `RN-R2`: total over any prefix, so `fold(log.slice(0, n))` is defined
282
- * for every `n` and `step(fold(prefix), next) === fold(prefix + next)`.
523
+ * for every `n` and `step(fold(prefix), next)` deep-equals `fold(prefix + next)`.
524
+ *
525
+ * **Linear in its inputs** — `RN-R43`, v0.29, [W194](../../doc/work-items/w194-the-fold-is-one-accumulator.md): ONE
526
+ * accumulator (`src/fold/accumulator.ts`, internal) has every input applied to it in place and is materialised into a
527
+ * plain state once, at the end. Before v0.29 this was a `step` per input, each copying the run, so a fold was quadratic
528
+ * in its run — 30 s for a 4,000-item map (P39 fact 1). What it returns is unchanged on every sequence whose region paths are branch paths (P39 decision 5; `RN-R43`'s one stated limit).
283
529
  *
284
530
  * `RN-R3`: read in **append order**, never sorted. Two settlements can share a
285
531
  * millisecond; `schedulerTimestamp` is the scheduler's clock and `observedAt` is
@@ -292,14 +538,26 @@ export declare function foldLog(log: readonly LogRecord[]): RunState;
292
538
  * Whether a run can still change — the predicate an L6 harness resolves on, and
293
539
  * the reason it needs no clock.
294
540
  *
295
- * `running` and `awaiting_action` are the two outcomes that say more input is
296
- * coming: a dispatched node has not settled, or a person has not answered. Every
297
- * other outcome is reached only when nothing is live, so the sequence is
298
- * finished. **This is `stuck`'s point:** before
541
+ * `running` and `awaiting_action` are two of the three outcomes that say more input is
542
+ * coming that can MOVE the run (`cancelling`, the third, below): a dispatched node has not settled, or a person has
543
+ * not answered. `completed`, `failed` and `stuck` are reached only when nothing is
544
+ * live. `halted` and — since W158 — `cancelled` are reached with dispatched nodes
545
+ * possibly still unsettled: the driver has HALTED the run, so what is in flight
546
+ * settles into `nodes` routed nowhere and moves no outcome. Either way the run is
547
+ * over. **This is `stuck`'s point:** before
299
548
  * [ADR 0015](../../doc/decisions/0015-a-run-whose-join-can-no-longer-fire-is-stuck.md)
300
549
  * a permanently stuck run was `completed`, so a harness waiting on this predicate
301
550
  * would have reported the hang as a success rather than waiting forever — which
302
551
  * is the worse of the two failures.
552
+ *
553
+ * **`cancelling` is the THIRD unfinished outcome since v0.18** (`RN-R12`, `RN-R30`, ADR 0104): the run's owner
554
+ * cancelled it and a task is still stopping, so its `task.cancelled` is still coming and moves the outcome to
555
+ * `cancelled`. Calling it terminal would settle a runtime's `done` at the request — before the tasks have stopped,
556
+ * the app's #1 complaint (P34 fact 6).
557
+ *
558
+ * **`interrupted` is TERMINAL, unchanged by v0.19** (`RN-R12`, `RN-R32`, ADR 0105): the run's owner stopped it and the
559
+ * driver went deaf, so nothing more can move it on THIS runtime — its in-flight set is moved by a resume on another,
560
+ * which is not this driver's input. A runtime's `done` settles on it once the stop's cancels have resolved (`RN-R33`).
303
561
  */
304
562
  export declare function isTerminal(s: RunState): boolean;
305
563
  /** A node's entry, or `null`. Not an outcome record: it has no `attempts` and no timing fields. */