fyflow-orchestration 0.7.0 → 0.8.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;
@@ -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 {
@@ -10,14 +10,19 @@
10
10
  import type { LogRecord, OutcomeRecord } from '../driver/log.js';
11
11
  import type { RegionEvent } from '../driver/events.js';
12
12
  /**
13
- * `spec/run-state.md` section 4. Four, not five.
13
+ * `spec/run-state.md` section 4. Five, not the scheduler's six.
14
14
  *
15
15
  * `user_action` is `suspended` here — it is not terminal at flow level. And there
16
16
  * is **no `pending`**: `OR-R4` says `task.running` writes no record, so the only
17
17
  * 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.
18
+ * moment. A state nothing can produce is not a state. *(Since v0.26 `task.running` produces a FOLD INPUT, `attempted`
19
+ * (`RN-R40`, W186) — still no record, and it moves no state: a task is `running` from its dispatch either way.)*
20
+ *
21
+ * **`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)),
22
+ * copied verbatim from the settlement as `done` and `failed` are. **Widening this union is BREAKING for a
23
+ * consumer switching on it exhaustively** — `resumed`'s and `gated`'s precedent.
19
24
  */
20
- export type NodeState = 'running' | 'done' | 'failed' | 'suspended';
25
+ export type NodeState = 'running' | 'done' | 'failed' | 'suspended' | 'cancelled';
21
26
  /**
22
27
  * `spec/run-state.md` section 3. Closed, and ours — an author labels why a path
23
28
  * ended, not what the run is.
@@ -26,17 +31,93 @@ export type NodeState = 'running' | 'done' | 'failed' | 'suspended';
26
31
  * [ADR 0015](../../doc/decisions/0015-a-run-whose-join-can-no-longer-fire-is-stuck.md):
27
32
  * nothing is live, nothing waits on a person, and a region's join has not fired,
28
33
  * so nothing will ever move again. v0.2 called that run **`completed`**.
34
+ *
35
+ * **`cancelled` is v0.15's seventh**, added by W158 under
36
+ * [ADR 0094](../../doc/decisions/0094-a-cancelled-task-is-a-terminal-state-that-routes-no-edge-and-ends-its-run-cancelled.md):
37
+ * a task of the run settled `cancelled`, so the driver halted it (`RN-R28`). **Widening this union is BREAKING
38
+ * for a consumer switching on it exhaustively** — `resumed`'s and `gated`'s precedent; a consumer comparing
39
+ * strings sees a value it never named, and `isTerminal` already calls it terminal.
40
+ *
41
+ * **`halted` is v0.16's NAME for the outcome 0.7.0 called `stopped`** — W171,
42
+ * [ADR 0102](../../doc/decisions/0102-a-run-the-driver-halts-is-halted-and-a-stored-stopped-reads-as-one.md), P34
43
+ * decision 2 and stop-and-ask (f): a refusal terminated the run (`OR-R10`, `RN-R8`). *Stop* is P34's resumable stop
44
+ * of a run (W174), so the old name leaves this union ENTIRELY: **BREAKING**, and loudly — a consumer's
45
+ * `outcome === 'stopped'` no longer compiles, rather than silently changing what it means. The count does not move.
46
+ *
47
+ * **`cancelling` is v0.18's EIGHTH**, added by W173 under
48
+ * [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md): the
49
+ * run's OWNER cancelled it (`driver.cancel`, a `cancelRequested` element) and a task is still stopping (`RN-R30`).
50
+ * **NOT terminal** — the third outcome {@link isTerminal} calls unfinished — so a runtime's `done` waits for the last
51
+ * task to stop. **BREAKING in 0.8.0** (P34 decision 10): a consumer switching on it exhaustively, or keying a
52
+ * `Record<RunOutcome, …>`, fails to compile; one comparing strings sees a value it never named.
53
+ *
54
+ * **`interrupted` is v0.19's NINTH**, added by W174 under
55
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md): the
56
+ * run's OWNER stopped it to resume it elsewhere (`driver.stop`, an `interrupted` element), and no `resumed` element has
57
+ * followed (`RN-R32`). **Terminal** — {@link isTerminal} needs no change, and a runtime's `done` settles on it once the
58
+ * stop's cancels have resolved — **and the one terminal outcome a resume ACCEPTS**. **BREAKING in 0.8.0** (P34 decision
59
+ * 10), as `cancelling` is.
29
60
  */
30
- export type RunOutcome = 'running' | 'completed' | 'failed' | 'awaiting_action' | 'stopped' | 'stuck';
61
+ export type RunOutcome = 'running' | 'completed' | 'failed' | 'awaiting_action' | 'halted' | 'stuck' | 'cancelled' | 'cancelling' | 'interrupted';
62
+ /**
63
+ * **Who cancelled the run, and why** — `RunState.cancelRequest`, `RN-R30`, v0.18. What the `cancelRequested` element
64
+ * carried: `actor` as the caller gave it, `reason` the caller's or `null` when it gave none.
65
+ */
66
+ export interface CancelRequest {
67
+ actor: string;
68
+ reason: string | null;
69
+ }
70
+ /**
71
+ * **Who stopped the run, and why** — `RunState.interruption`, `RN-R32`, v0.19. What the `interrupted` element carried:
72
+ * `actor` as the caller gave it, `reason` the caller's or `null` when it gave none.
73
+ */
74
+ export interface Interruption {
75
+ actor: string;
76
+ reason: string | null;
77
+ }
31
78
  /** How a branch left its region. `null` while it is still live. */
32
79
  export type BranchSettlement = 'arrived' | 'exited' | 'lost';
80
+ /**
81
+ * **One task's entry in `RunState.nodes`** — `RN-R5`, and since v0.27 its ATTRIBUTION as fields (`RN-R41`,
82
+ * [ADR 0114](../../doc/decisions/0114-a-tasks-attribution-is-fields-and-its-messages-and-progress-are-passed-through.md), P36
83
+ * stop-and-ask (j)): the run, the flow-level attempt and the scheduler's attempt number, so a reader holding an entry on
84
+ * its own needs no parse of its key. **`runId`, `flowAttempt` and `attemptNumber` are REQUIRED, and that is BREAKING in
85
+ * 0.8.0 for code that builds a `NodeRun` literal** (P36 (p)); a reader is unaffected. Fold OUTPUT only: no fold input
86
+ * carries a field derived from the task id (`spec/outcome-record.md` section 2), and none moves.
87
+ */
33
88
  export interface NodeRun {
89
+ /**
90
+ * **The run the task belongs to** — v0.27, `RN-R41`: the task id's first segment (`spec/task-id.md` section 1), equal to
91
+ * `RunState.runId` on every entry of a run's fold. On the entry so it travels with it.
92
+ */
93
+ runId: string;
34
94
  nodeId: string;
35
95
  /** `'-'` when the node is in no fan-out region. */
36
96
  branchPath: string;
97
+ /**
98
+ * **The FLOW-LEVEL attempt** — v0.27, `RN-R41`: the task id's fourth segment, `0` on every id this library mints today
99
+ * ([Q16](../../doc/questions/q16.md)'s flow half mints nothing above it). NOT the scheduler's attempt number: that is
100
+ * {@link NodeRun.attemptNumber}, named apart so the two cannot be confused (P36 (j)).
101
+ */
102
+ flowAttempt: number;
103
+ /**
104
+ * **The SCHEDULER's attempt number** — v0.27, `RN-R41`: the 1-based `attemptNumber` the LATEST `attempted` element for this
105
+ * task carried (`RN-R40`), so a retried task reads the attempt it is on, or settled on. **`null`** before its first
106
+ * attempt starts, on an ASK (never handed to a scheduler), and on every entry of a log written before 0.8.0, which holds
107
+ * no `attempted` element — never derived from a settlement's `attempts`.
108
+ */
109
+ attemptNumber: number | null;
37
110
  state: NodeState;
38
111
  result: unknown;
39
112
  error: string | null;
113
+ /**
114
+ * **What REACHED an ask** — `RN-R36`, v0.22, P35 (f),
115
+ * [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md): the value
116
+ * its `needs` composed (or the edge's value without one), as its `asked` element carried it, and KEPT after the answer.
117
+ * **Present only on an ask's node**, so every other node's entry has the five fields it always had — OPTIONAL, and
118
+ * additive: a consumer building a `NodeRun` literal is unaffected.
119
+ */
120
+ input?: unknown;
40
121
  }
41
122
  export interface RegionRun {
42
123
  /** Branch paths opened, in the order the driver emitted them (`JP-R5`). */
@@ -95,8 +176,37 @@ export interface RunState {
95
176
  reason: 'end' | 'no-eligible-edge';
96
177
  at: string;
97
178
  }[];
98
- /** Set when a refusal terminated the run (`OR-R10`, `RN-R8`). */
99
- stoppedBy: string | null;
179
+ /**
180
+ * Set when a refusal HALTED the run (`OR-R10`, `RN-R8`): the refusal's class, `: `, and its message. *(0.7.0's
181
+ * `stoppedBy` — renamed with the outcome by ADR 0102; BREAKING, and a consumer reading the old name fails to compile.)*
182
+ */
183
+ haltedBy: string | null;
184
+ /**
185
+ * **Set when the run's OWNER cancelled it** — `RN-R30`, v0.18, W173,
186
+ * [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md): the
187
+ * FIRST `cancelRequested` element's `{ actor, reason }`, and `null` for a run nobody cancelled — including one a cancel
188
+ * the scheduler made on its own ended (`RN-R28`), which carries no actor. **A new cell, BREAKING in 0.8.0 for a
189
+ * consumer that builds a `RunState` literal** (P34 decision 10).
190
+ */
191
+ cancelRequest: CancelRequest | null;
192
+ /**
193
+ * **Set while the run's OWNER has STOPPED it** — `RN-R32`, v0.19, W174,
194
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md): the
195
+ * `interrupted` element's `{ actor, reason }`, and `null` again once a `resumed` element follows it. **CURRENT, not
196
+ * cumulative** — `regions[*].suspended`'s precedent (`RN-R6`): that a resumed run was ever stopped is in the input
197
+ * sequence, not here. **A new cell, BREAKING in 0.8.0 for a consumer that builds a `RunState` literal** (P34 decision 10).
198
+ */
199
+ interruption: Interruption | null;
200
+ /**
201
+ * **Whether the run's OWNER has paused it NOW** — `RN-R34`, v0.20, W175,
202
+ * [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md): `true`
203
+ * from a `paused` element until an `unpaused` one. **A flag, NOT an outcome** (P34 (e)): a paused run still routes, and
204
+ * each dispatch it holds is `dispatched` and so `running` in `nodes` — so `outcome` is exactly what it would be without
205
+ * the pause, and no precedence moves. **CURRENT, not cumulative**: that a run was ever paused is in the input sequence.
206
+ * Neither a stop, a cancel nor a resume lifts it — only an `unpaused` element does, so a run stopped while paused
207
+ * resumes paused. **A new cell, BREAKING in 0.8.0 for a consumer that builds a `RunState` literal** (P34 decision 10).
208
+ */
209
+ paused: boolean;
100
210
  /**
101
211
  * **Every resume this run has had, in order, each the task ids it handed a scheduler AGAIN** —
102
212
  * `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 +277,7 @@ export interface GateDecision {
167
277
  *
168
278
  * Neither is a log record, and neither belongs in one: `spec/outcome-record.md`
169
279
  * 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.
280
+ * already carried region events and a halt for the same reason.
171
281
  *
172
282
  * **`joinSettled` is W5's, and it is the third fact of this shape**
173
283
  * ([ADR 0015](../../doc/decisions/0015-a-run-whose-join-can-no-longer-fire-is-stuck.md)):
@@ -221,6 +331,25 @@ export type FoldInput = {
221
331
  } | {
222
332
  kind: 'dispatched';
223
333
  taskId: string;
334
+ }
335
+ /**
336
+ * **A branch reached an ASK, which parked** — `RN-R36`, `RN-R37`, v0.22,
337
+ * [W180](../../doc/work-items/w180-asking-and-answering-in-a-run.md),
338
+ * [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md), P35
339
+ * stop-and-ask (b): appended by the driver where a branch ENTERS an ask, with NOTHING handed to the scheduler — `taskId`
340
+ * the ask's own (`spec/task-id.md`'s scheme, attempt `0`) and `input` what reached it, composed through its `needs`
341
+ * (P35 (f)). The fold reads the node `suspended` from here, its `input` kept, until the person's `resolution`.
342
+ *
343
+ * **A structural fact, not a terminal state** (P35 decision 5, fact 13): `dispatched`'s precedent — no outcome record is
344
+ * ever written for an ask, and its only terminal fact is the person's answer. **REGENERATED on resume, never FED**
345
+ * (`RN-R19`): the replay re-enters the ask and compares the input it composes with the stored one (`RN-R20`). **The
346
+ * first kind whose payload is a value of ANY shape**, so it is deep-copied on the way out (`freezeInput`, `RN-R17`).
347
+ * **A new kind, BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P35 decision 8).
348
+ */
349
+ | {
350
+ kind: 'asked';
351
+ taskId: string;
352
+ input: unknown;
224
353
  } | {
225
354
  kind: 'pathEnded';
226
355
  reason: 'end' | 'no-eligible-edge';
@@ -231,10 +360,77 @@ export type FoldInput = {
231
360
  regionPath: string;
232
361
  joinId: string;
233
362
  fired: boolean;
234
- } | {
235
- kind: 'stopped';
363
+ }
364
+ /**
365
+ * **A refusal HALTED the run** — `OR-R10`, `RN-R8`; appended by the driver's `failRun`, `reason` the refusal's class,
366
+ * `: ` and its message. **0.7.0 wrote this element as `{ kind: 'stopped', reason }`** and ADR 0102 renamed it
367
+ * (W171, P34 stop-and-ask (f)). **The old kind is READ, never written, and is not a member of this union**: a stored
368
+ * sequence is data from before the rename, so {@link step} folds an element whose `kind` is `'stopped'` exactly as
369
+ * this one and `resumeRefusal` refuses it `halted` at its position — `CHECKPOINT_FORMAT` stays `1`, and a 0.7.0
370
+ * checkpoint of a halted run is refused for the right reason while one of a live run still resumes. Nothing in this
371
+ * library writes `'stopped'` again, and the type says so: a consumer's `kind === 'stopped'` no longer compiles.
372
+ * **BREAKING in 0.8.0** (P34 decision 10): a consumer that switched on, compared or CONSTRUCTED the old kind fails
373
+ * to compile.
374
+ */
375
+ | {
376
+ kind: 'halted';
236
377
  reason: string;
237
378
  }
379
+ /**
380
+ * **The run's OWNER cancelled it** — `RN-R30`, `RN-R31`, v0.18,
381
+ * [W173](../../doc/work-items/w173-cancelling-a-run.md),
382
+ * [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md), P34
383
+ * decision 4: appended ONCE by `Driver.cancel`, after the hold and BEFORE the scheduler is asked to cancel anything,
384
+ * so every `task.cancelled` it causes folds under it. `reason` is `null` when the caller gave none. The fold reads the
385
+ * run `cancelling` from here while a dispatched task is unsettled, and `cancelled` once none is.
386
+ *
387
+ * **Never fed to a replay**: a resume refuses a stored one `cancelled` before replaying anything (`RN-R23`). **A new
388
+ * kind, BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P34 decision 10).
389
+ */
390
+ | {
391
+ kind: 'cancelRequested';
392
+ actor: string;
393
+ reason: string | null;
394
+ }
395
+ /**
396
+ * **The run's OWNER stopped it, to resume it elsewhere** — `RN-R32`, `RN-R33`, v0.19,
397
+ * [W174](../../doc/work-items/w174-stopping-a-run-to-resume-it-elsewhere.md),
398
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md), P34
399
+ * decision 3 and stop-and-ask (d): appended ONCE by `Driver.stop`, after the hold and the driver going DEAF and BEFORE
400
+ * 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
401
+ * caller gave none. The fold reads the run `interrupted` from here until a `resumed` element.
402
+ *
403
+ * **FED to a replay** (`RN-R19`), at its own position — the one terminal element a resume does not refuse. **A new
404
+ * kind, BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P34 decision 10).
405
+ */
406
+ | {
407
+ kind: 'interrupted';
408
+ actor: string;
409
+ reason: string | null;
410
+ }
411
+ /**
412
+ * **The run's OWNER paused it** — `RN-R34`, `RN-R35`, v0.20,
413
+ * [W175](../../doc/work-items/w175-pausing-a-run.md),
414
+ * [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md), P34
415
+ * stop-and-ask (e): appended ONCE by `Driver.pause`, at rest — between two of the driver's steps — and from it on every
416
+ * dispatch is appended and HELD. The fold sets `RunState.paused`; no outcome moves.
417
+ *
418
+ * **FED to a replay** (`RN-R19`), at its own position, so a run stopped while paused resumes paused. **A new kind,
419
+ * BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P34 decision 10).
420
+ */
421
+ | {
422
+ kind: 'paused';
423
+ actor: string;
424
+ }
425
+ /**
426
+ * **The run's OWNER unpaused it** — `RN-R34`, `RN-R35`, v0.20, W175, ADR 0106: appended ONCE by `Driver.unpause`, at
427
+ * rest, BEFORE the held tasks are handed on, in dispatch order. The fold clears `RunState.paused`. **FED to a replay**;
428
+ * **a new kind, BREAKING in 0.8.0** (P34 decision 10).
429
+ */
430
+ | {
431
+ kind: 'unpaused';
432
+ actor: string;
433
+ }
238
434
  /**
239
435
  * **The run was RESUMED, and these are the tasks handed to a scheduler again** — `RN-R22`,
240
436
  * [W89](../../doc/work-items/w89-what-resuming-is.md), P23 stop-and-ask (a), answered: *a resume
@@ -269,7 +465,30 @@ export type FoldInput = {
269
465
  */
270
466
  | ({
271
467
  kind: 'gated';
272
- } & GateDecision);
468
+ } & GateDecision)
469
+ /**
470
+ * **An ATTEMPT of a task started** — `RN-R40`, v0.26,
471
+ * [W186](../../doc/work-items/w186-attempts-recorded-and-carried-across-a-resume.md),
472
+ * [ADR 0113](../../doc/decisions/0113-a-tasks-attempts-are-recorded-and-carried-across-a-resume.md), P36 stop-and-ask (f):
473
+ * appended by the driver for the scheduler's `task.running` of a task THIS run dispatched, carrying the scheduler's
474
+ * 1-based `attemptNumber` — and ONLY when that number EXCEEDS the last one recorded for the task, so a requeue (which
475
+ * repeats its number) appends nothing. For every task, with or without a `retryPolicy`. **Appended at REST**: a
476
+ * `task.running` heard on the driver's own step — an inline pool fires it INSIDE `addTask` — is appended when that step
477
+ * ends, so the element always lies BETWEEN two steps, where a resume's replay feeds it (`RN-R19`).
478
+ *
479
+ * **FED to a replay** (`RN-R19`'s seventh kind): only the scheduler can produce it, so the replay cannot regenerate it;
480
+ * the highest number the replay was fed for a task is what a resume re-hands it with as `attemptsSpent` (`RN-R21`), so a
481
+ * task's retry budget is NOT reset by a crash (Q365). **Moved nothing in `RunState` at v0.26; since v0.27 it sets ONE
482
+ * cell, its task's `NodeRun.attemptNumber`** (`RN-R41`, W187) — the run's outcome, every state and every other cell are
483
+ * what they would be without it. **Never a log record**
484
+ * (`OR-R4`: `task.running` writes none) — `cancelRequested`'s precedent. **A new kind, BREAKING in 0.8.0 for a consumer
485
+ * switching on `kind` exhaustively** (P36 decision 8, (p)).
486
+ */
487
+ | {
488
+ kind: 'attempted';
489
+ taskId: string;
490
+ attemptNumber: number;
491
+ };
273
492
  export declare function emptyState(): RunState;
274
493
  /**
275
494
  * One step. **Returns a new state; never mutates the one it was given** — which is
@@ -292,14 +511,26 @@ export declare function foldLog(log: readonly LogRecord[]): RunState;
292
511
  * Whether a run can still change — the predicate an L6 harness resolves on, and
293
512
  * the reason it needs no clock.
294
513
  *
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
514
+ * `running` and `awaiting_action` are two of the three outcomes that say more input is
515
+ * coming that can MOVE the run (`cancelling`, the third, below): a dispatched node has not settled, or a person has
516
+ * not answered. `completed`, `failed` and `stuck` are reached only when nothing is
517
+ * live. `halted` and — since W158 — `cancelled` are reached with dispatched nodes
518
+ * possibly still unsettled: the driver has HALTED the run, so what is in flight
519
+ * settles into `nodes` routed nowhere and moves no outcome. Either way the run is
520
+ * over. **This is `stuck`'s point:** before
299
521
  * [ADR 0015](../../doc/decisions/0015-a-run-whose-join-can-no-longer-fire-is-stuck.md)
300
522
  * a permanently stuck run was `completed`, so a harness waiting on this predicate
301
523
  * would have reported the hang as a success rather than waiting forever — which
302
524
  * is the worse of the two failures.
525
+ *
526
+ * **`cancelling` is the THIRD unfinished outcome since v0.18** (`RN-R12`, `RN-R30`, ADR 0104): the run's owner
527
+ * cancelled it and a task is still stopping, so its `task.cancelled` is still coming and moves the outcome to
528
+ * `cancelled`. Calling it terminal would settle a runtime's `done` at the request — before the tasks have stopped,
529
+ * the app's #1 complaint (P34 fact 6).
530
+ *
531
+ * **`interrupted` is TERMINAL, unchanged by v0.19** (`RN-R12`, `RN-R32`, ADR 0105): the run's owner stopped it and the
532
+ * driver went deaf, so nothing more can move it on THIS runtime — its in-flight set is moved by a resume on another,
533
+ * which is not this driver's input. A runtime's `done` settles on it once the stop's cancels have resolved (`RN-R33`).
303
534
  */
304
535
  export declare function isTerminal(s: RunState): boolean;
305
536
  /** A node's entry, or `null`. Not an outcome record: it has no `attempts` and no timing fields. */
@@ -8,7 +8,7 @@
8
8
  * decision 5, [W59](../../doc/work-items/w59-what-a-node-type-is.md), [W65](../../doc/work-items/w65-the-registry-document-and-a-node-type-is-a-derivation.md);
9
9
  * **and since v0.18 the place a language adapter's WORKER half reaches a typed pool** —
10
10
  * [ADR 0051](../../doc/decisions/0051-a-transform-is-a-declared-expression-in-a-registered-language-applied-in-the-types-worker-wrapper.md)
11
- * rule 4, [W61](../../doc/work-items/w61-what-a-transform-is.md).
11
+ * rule 4, [W61](../../doc/work-items/w61-what-a-transform-is.md) — into EVERY typed pool since ADR 0096 (W162).
12
12
  *
13
13
  * **This sub-path is the one place the published package CONSTRUCTS a scheduler object** (`RT-R13`;
14
14
  * `createRuntime` in `./runtime.ts` is the other module in it, and it constructs the groups and the
@@ -21,7 +21,8 @@
21
21
  * **Since W65 the helper takes the REGISTRY's three sections — `workers`, `nodeTypes`, `pools` —
22
22
  * and builds one pool per INSTANCE**: the default instance of every type, keyed by the type's name,
23
23
  * and every named instance in `pools`, keyed as declared. Each is the type's derivation run over the
24
- * instance's config (`derivePool`, the same checks `resolveRegistry` makes), turned into
24
+ * instance's config — and, since v0.37, a document type's instance's own `pool` options merged over the type's
25
+ * (ADR 0099) — (`derivePool`, the same checks `resolveRegistry` makes), turned into
25
26
  * `WorkerManagerOptions` by `poolOptionsOf` — the type's `inline`, the derived pool fields and
26
27
  * groups, and the envelope pool config `{ worker: <the worker entry's url>, config: <the derived
27
28
  * config> }` — and handed to the pool factory with the package's envelope worker as its script.
@@ -35,21 +36,23 @@
35
36
  * nothing. Since W65 a POOL's groups are what its derivation returned (`groups` and `residentGroups`),
36
37
  * which for a document type is what the type declared.
37
38
  *
38
- * **The languages are written into a pool's config ONLY for a type some node of which declares a
39
- * transform, and only the languages those nodes name** (W61, P17 decision 3): the helper reads the
40
- * `flows` it is given, collects each type's transforming languages, and writes their WORKER-side
41
- * module URLs (`LanguageAdapter.worker`) into every instance of that type — a FUNCTION could not cross
42
- * to a thread and a URL can (P17 fact 7). A type whose nodes never transform gets the two-key pool
43
- * config it had at v0.17, so no pool loads an engine it does not use. Direction one is a refusal;
44
- * direction two is NOT — an adapter given that no node names is inert (ADR 0051 rule 4's asymmetry).
39
+ * **Every pool carries every language it is given** — [ADR 0096](../../doc/decisions/0096-a-runtime-is-its-registry-and-start-refuses-from-runtime-check.md)
40
+ * rule 2, [W162](../../doc/work-items/w162-a-runtimes-languages-are-its-registrys-and-runtime-check.md),
41
+ * superseding ADR 0051 rule 4. The helper writes each adapter's WORKER-side module URL
42
+ * (`LanguageAdapter.worker`) into EVERY pool it builds, under the language's name — a FUNCTION could
43
+ * not cross to a thread and a URL can (P17 fact 7). It reads no flow: a runtime's pools are built once
44
+ * and a flow published later may transform on any type, so which types transform is not knowable here.
45
+ * Given no language, every pool config is the two-key one it had at v0.17 (P18 fact 5).
45
46
  */
46
47
  import { WorkerManager } from 'fyflow-scheduler';
47
48
  import type { ResourceGroup, WorkerManagerOptions } from 'fyflow-scheduler';
48
- import type { FlowDefinition, Languages, NodeTypes, Pools, ResolvedPool, Workers } from '../definition/types.js';
49
+ import type { Languages, NodeTypes, Pools, ResolvedPool, Workers } from '../definition/types.js';
49
50
  /**
50
- * The script every typed pool loads. Resolved against THIS module, so it is the published
51
- * package's own file wherever the package is installed; the type's real worker is loaded from
52
- * inside it (`EnvelopeWorker.setup`).
51
+ * The script every typed pool loads; the type's real worker is loaded from inside it
52
+ * (`EnvelopeWorker.setup`). In the source it resolves against THIS module, the package's own file
53
+ * wherever it is installed — so a bundle built from the JSR source must serve it beside itself. The
54
+ * npm dist replaces it at build time with the worker itself: a `data:` URL on Node, a `Blob` URL in
55
+ * the browser (`scripts/build.mjs`, Q580).
53
56
  */
54
57
  export declare const ENVELOPE_WORKER_URL: string;
55
58
  /** A registry that cannot be bootstrapped, and why — thrown before any pool is built. A registry SHAPE defect is a `RegistryError` instead, from `checkRegistry`. */
@@ -70,15 +73,10 @@ export interface BootstrapOptions {
70
73
  createPool?: PoolFactory;
71
74
  /**
72
75
  * **The registry's languages — the same object the driver is handed** (W61). Read for ONE field,
73
- * `worker`, and only for the languages some node in `flows` declares on a node of each type.
76
+ * `worker`, and written into EVERY pool (ADR 0096 rule 2). Omitted or empty, no pool config carries
77
+ * a `languages` key. *(A `flows` option sat beside this until W162 and is removed — ADR 0096 rule 3.)*
74
78
  */
75
79
  languages?: Languages;
76
- /**
77
- * **The flows these pools will run**, read for WHICH types transform in WHICH languages. Omitted,
78
- * or naming no transform, every pool config is exactly what it was at v0.17. A node naming a
79
- * type this registry lacks is skipped here — it is the driver's `FD-V32`, not this helper's.
80
- */
81
- flows?: readonly FlowDefinition[];
82
80
  }
83
81
  export interface BootstrapResult {
84
82
  /** One per pool INSTANCE — every type's default instance under the type's name, every named instance under its key — spread into `new FyflowScheduler(...)`'s first argument. */
@@ -88,10 +86,4 @@ export interface BootstrapResult {
88
86
  /** What each pool was DERIVED as — the checked spec every pool above was built from, for a caller that reads pools back (ladder L6's observers). */
89
87
  derived: Record<string, ResolvedPool>;
90
88
  }
91
- /**
92
- * **Which languages each type's nodes transform in, over the flows given** — the one reading of a
93
- * definition this helper makes. Exported so a test can compare what the helper WROTE into pool
94
- * configs against what the flows DECLARE, both ways (rule 8 obligation (e) on `Registry.languages`).
95
- */
96
- export declare function transformLanguagesByType(flows: readonly FlowDefinition[]): ReadonlyMap<string, ReadonlySet<string>>;
97
89
  export declare function bootstrap(registry: BootstrapRegistry, groups?: Readonly<Record<string, ResourceGroup>>, options?: BootstrapOptions): BootstrapResult;
@@ -10,8 +10,8 @@
10
10
  * A `WorkerManager` loads its worker from a script URL and instantiates the module's default
11
11
  * export, so a wrapper cannot be a function around a class — it has to BE the script the pool
12
12
  * loads. This module is that script: its pool config names the type's real worker (`worker`, a
13
- * module URL), that worker's own config (`config`), and — for a type some node of which declares
14
- * a transform — the worker-side module URL of each language those nodes name (`languages`); this
13
+ * module URL), that worker's own config (`config`), and the worker-side module URL of every language
14
+ * the registry names (`languages` — every typed pool's since ADR 0096, W162); this
15
15
  * class loads the inner worker and the language modules in `setup()`, evaluates the envelope's
16
16
  * `transform` in `run()` when one is present, hands the inner worker **exactly `{ settings, input }`**
17
17
  * with `input` REPLACED by the evaluation's result, and tears the inner worker down with itself.
@@ -74,8 +74,8 @@ export interface EnvelopePoolConfig {
74
74
  config?: unknown;
75
75
  /**
76
76
  * **Language name → worker-side module URL** (`LanguageAdapter.worker`), written by the bootstrap
77
- * helper ONLY for a type some node of which declares a `transform`, and only for the languages
78
- * those nodes name — so a pool whose nodes never transform loads no engine. Loaded in `setup()`,
77
+ * helper into EVERY pool, for every language it is given — ADR 0096 rule 2 (W162), superseding
78
+ * ADR 0051 rule 4's *only for a type some node of which declares a transform*. Loaded in `setup()`,
79
79
  * once per worker instance; each module's default export is `compile(expr) => { evaluate(input,
80
80
  * settings) }`.
81
81
  */
@@ -188,7 +188,8 @@ export default class EnvelopeWorker {
188
188
  /** One language's worker half, or the Error that stands in for it — never a throw (see `setup()`). */
189
189
  private loadLanguage;
190
190
  /**
191
- * `FD-C5`: the inner worker receives `{ settings, input }` and nothing else. `FD-C6`: when the
191
+ * `FD-C5`: the inner worker receives `{ settings, input }` as its payload, and the scheduler's task context as its
192
+ * second argument, passed through untouched (W159). `FD-C6`: when the
192
193
  * envelope carries a `transform`, `input` is the evaluation of that expression over the value
193
194
  * received, with the node's settings bound as `$settings`, and the third key is consumed here.
194
195
  */
@@ -5,9 +5,9 @@
5
5
  * for everything else. Since v0.18 (W61, ADR 0051) the envelope worker here also evaluates a
6
6
  * node's declared `transform`, and `TransformFailed` is what a task fails with when it cannot.
7
7
  */
8
- export { bootstrap, BootstrapError, ENVELOPE_WORKER_URL, transformLanguagesByType } from './bootstrap.js';
8
+ export { bootstrap, BootstrapError, ENVELOPE_WORKER_URL } from './bootstrap.js';
9
9
  export type { BootstrapOptions, BootstrapRegistry, BootstrapResult, PoolFactory } from './bootstrap.js';
10
- export { BUILT_IN_GROUP_FACTORIES, createRuntime, GROUP_MEMBERS, GroupNotBuildable, GroupUnusable, LanguageNotBuilt, RunIdInUse, RuntimeError } from './runtime.js';
10
+ export { BUILT_IN_GROUP_FACTORIES, createRuntime, GROUP_MEMBERS, GroupNotBuildable, GroupUnusable, LimitKeyMissing, RunIdInUse, RuntimeError } from './runtime.js';
11
11
  export type { ResumeOptions, RunHandle, Runtime, RuntimeOptions, RuntimeRegistry, StartOptions } from './runtime.js';
12
12
  export { default as EnvelopeWorker, NotAnEnvelope, TransformFailed, unpackEnvelope } from './envelope-worker.js';
13
13
  export type { Envelope, EnvelopePoolConfig } from './envelope-worker.js';