@demlik/tea 0.1.1 → 0.1.2

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.
Files changed (145) hide show
  1. package/dist/agent/index.d.ts +2 -2
  2. package/dist/agent/index.js +8 -8
  3. package/dist/authed-call/index.d.ts +3 -3
  4. package/dist/authed-call/index.js +5 -5
  5. package/dist/await-terminal/index.d.ts +1 -1
  6. package/dist/await-terminal/index.js +2 -2
  7. package/dist/batch-window/index.d.ts +2 -2
  8. package/dist/batch-window/index.js +4 -4
  9. package/dist/cache/index.d.ts +2 -2
  10. package/dist/{chunk-RVJFGRGK.js → chunk-27N2SPTN.js} +4 -4
  11. package/dist/chunk-27N2SPTN.js.map +1 -0
  12. package/dist/{chunk-FH5RUIIL.js → chunk-3XJWC7VW.js} +3 -3
  13. package/dist/{chunk-FH5RUIIL.js.map → chunk-3XJWC7VW.js.map} +1 -1
  14. package/dist/{chunk-IFBQJYGI.js → chunk-5DEQ5I3U.js} +3 -3
  15. package/dist/{chunk-IFBQJYGI.js.map → chunk-5DEQ5I3U.js.map} +1 -1
  16. package/dist/{chunk-XRXKH7KA.js → chunk-6RGBKODV.js} +3 -3
  17. package/dist/{chunk-XRXKH7KA.js.map → chunk-6RGBKODV.js.map} +1 -1
  18. package/dist/{chunk-D5EQDLDV.js → chunk-7W4LMPUW.js} +4 -4
  19. package/dist/{chunk-D5EQDLDV.js.map → chunk-7W4LMPUW.js.map} +1 -1
  20. package/dist/{chunk-A56GNPI7.js → chunk-DILMKOZ7.js} +5 -23
  21. package/dist/chunk-DILMKOZ7.js.map +1 -0
  22. package/dist/{chunk-EIZJTYKR.js → chunk-EHIX7NYZ.js} +4 -4
  23. package/dist/{chunk-EIZJTYKR.js.map → chunk-EHIX7NYZ.js.map} +1 -1
  24. package/dist/{chunk-WUV3PSCZ.js → chunk-HCEVHFJ7.js} +2 -2
  25. package/dist/chunk-HCEVHFJ7.js.map +1 -0
  26. package/dist/{chunk-EQRUAP2A.js → chunk-J6527VRQ.js} +2 -2
  27. package/dist/chunk-J6527VRQ.js.map +1 -0
  28. package/dist/{chunk-QGZW4W3E.js → chunk-K7Q23EQ4.js} +3 -3
  29. package/dist/{chunk-QGZW4W3E.js.map → chunk-K7Q23EQ4.js.map} +1 -1
  30. package/dist/{chunk-RGTPPF4U.js → chunk-LP7VQBRG.js} +3 -3
  31. package/dist/{chunk-RGTPPF4U.js.map → chunk-LP7VQBRG.js.map} +1 -1
  32. package/dist/{chunk-2WZHS4P2.js → chunk-MMH7CW5K.js} +4 -16
  33. package/dist/chunk-MMH7CW5K.js.map +1 -0
  34. package/dist/{chunk-EDGDR5XX.js → chunk-ON24RKSE.js} +3 -3
  35. package/dist/{chunk-EDGDR5XX.js.map → chunk-ON24RKSE.js.map} +1 -1
  36. package/dist/{chunk-TAO4YOLO.js → chunk-PS23H6KQ.js} +3 -3
  37. package/dist/{chunk-TAO4YOLO.js.map → chunk-PS23H6KQ.js.map} +1 -1
  38. package/dist/{chunk-BNJYE4AU.js → chunk-QLA76GIT.js} +21 -16
  39. package/dist/chunk-QLA76GIT.js.map +1 -0
  40. package/dist/{chunk-TC4HWABA.js → chunk-S3FQN4SL.js} +7 -7
  41. package/dist/{chunk-TC4HWABA.js.map → chunk-S3FQN4SL.js.map} +1 -1
  42. package/dist/{chunk-RVU3RBE6.js → chunk-SQTCS44W.js} +6 -4
  43. package/dist/chunk-SQTCS44W.js.map +1 -0
  44. package/dist/{chunk-7VEG4KHB.js → chunk-VLGARN6H.js} +3 -3
  45. package/dist/{chunk-7VEG4KHB.js.map → chunk-VLGARN6H.js.map} +1 -1
  46. package/dist/{core-CymXZCeY.d.ts → core-BoB6t7dB.d.ts} +11 -1
  47. package/dist/deadline/index.d.ts +2 -2
  48. package/dist/deadline/index.js +3 -3
  49. package/dist/debounce/index.d.ts +6 -0
  50. package/dist/debounce/index.js +1 -1
  51. package/dist/do/index.d.ts +15 -24
  52. package/dist/do/index.js +9 -13
  53. package/dist/do/index.js.map +1 -1
  54. package/dist/{durable-effects-2MDxDs45.d.ts → durable-effects-CO9Uh971.d.ts} +1 -1
  55. package/dist/extension/index.d.ts +1 -1
  56. package/dist/extension/index.js +3 -3
  57. package/dist/extension/react.js +3 -3
  58. package/dist/extension/subs/index.d.ts +2 -2
  59. package/dist/fan-out/index.d.ts +1 -1
  60. package/dist/idempotent-intake/index.d.ts +1 -1
  61. package/dist/index.d.ts +2 -2
  62. package/dist/index.js +2 -2
  63. package/dist/llm-call/index.d.ts +2 -2
  64. package/dist/llm-call/index.js +5 -5
  65. package/dist/machine-viz/index.d.ts +4 -4
  66. package/dist/machine-viz/index.js +4 -8
  67. package/dist/machine-viz/index.js.map +1 -1
  68. package/dist/mem/index.d.ts +1 -1
  69. package/dist/monitored-run/index.d.ts +2 -2
  70. package/dist/monitored-run/index.js +5 -5
  71. package/dist/node/index.d.ts +1 -1
  72. package/dist/paginated-walk/index.d.ts +2 -2
  73. package/dist/paginated-walk/index.js +5 -5
  74. package/dist/parity/index.d.ts +10 -4
  75. package/dist/parity/index.js +6 -5
  76. package/dist/parity/index.js.map +1 -1
  77. package/dist/pbt/arbitraries/index.d.ts +1 -1
  78. package/dist/pbt/index.d.ts +15 -8
  79. package/dist/pbt/index.js +5 -11
  80. package/dist/pbt/index.js.map +1 -1
  81. package/dist/pbt/runners/index.d.ts +1 -1
  82. package/dist/pbt/runners/index.js +3 -3
  83. package/dist/poller/index.d.ts +2 -2
  84. package/dist/poller/index.js +3 -3
  85. package/dist/prediction/index.d.ts +23 -2
  86. package/dist/prediction/index.js +2 -2
  87. package/dist/pure/index.d.ts +2 -2
  88. package/dist/pure/index.js +2 -2
  89. package/dist/raft/demo.d.ts +2 -2
  90. package/dist/raft/demo.js +5 -5
  91. package/dist/raft/index.d.ts +3 -3
  92. package/dist/raft/index.js +4 -4
  93. package/dist/raft/sim.d.ts +2 -2
  94. package/dist/raft/sim.js +5 -5
  95. package/dist/raft/timeline.d.ts +2 -2
  96. package/dist/react/index.d.ts +1 -1
  97. package/dist/react/index.js +8 -4
  98. package/dist/react/index.js.map +1 -1
  99. package/dist/reconciler/index.d.ts +2 -2
  100. package/dist/reconciler/index.js +5 -5
  101. package/dist/recorder/index.d.ts +1 -1
  102. package/dist/resilient-call/index.d.ts +45 -2
  103. package/dist/resilient-call/index.js +4 -4
  104. package/dist/retry-to-success/index.d.ts +109 -0
  105. package/dist/retry-to-success/index.js +35 -0
  106. package/dist/retry-to-success/index.js.map +1 -0
  107. package/dist/saga/index.d.ts +14 -1
  108. package/dist/saga/index.js.map +1 -1
  109. package/dist/snapshot/index.d.ts +1 -1
  110. package/dist/snapshot/index.js +3 -3
  111. package/dist/subs/index.d.ts +2 -2
  112. package/dist/testing/index.d.ts +1 -1
  113. package/dist/testing/index.js +2 -2
  114. package/dist/throttle/index.d.ts +9 -0
  115. package/dist/throttle/index.js +1 -1
  116. package/dist/throttled-input/index.d.ts +10 -2
  117. package/dist/throttled-input/index.js +6 -6
  118. package/dist/throttled-input/index.js.map +1 -1
  119. package/dist/token-refresh/index.d.ts +1 -1
  120. package/dist/token-refresh/index.js +3 -3
  121. package/dist/trace-replay/index.d.ts +1 -1
  122. package/dist/trace-replay/index.js +3 -3
  123. package/dist/{types-N9LZbdnr.d.ts → types-B-8bcAp8.d.ts} +1 -1
  124. package/dist/with-deadline/index.d.ts +1 -1
  125. package/dist/with-deadline/index.js +5 -24
  126. package/dist/with-deadline/index.js.map +1 -1
  127. package/dist/with-resilience/index.d.ts +2 -2
  128. package/dist/with-resilience/index.js +13 -36
  129. package/dist/with-resilience/index.js.map +1 -1
  130. package/dist/with-telemetry/index.d.ts +1 -1
  131. package/dist/with-telemetry/index.js +4 -29
  132. package/dist/with-telemetry/index.js.map +1 -1
  133. package/dist/work-queue/adapter.d.ts +1 -1
  134. package/dist/work-queue/index.d.ts +1 -1
  135. package/dist/work-queue/ops.d.ts +1 -1
  136. package/dist/workflow/index.d.ts +16 -10
  137. package/dist/workflow/index.js.map +1 -1
  138. package/package.json +8 -2
  139. package/dist/chunk-2WZHS4P2.js.map +0 -1
  140. package/dist/chunk-A56GNPI7.js.map +0 -1
  141. package/dist/chunk-BNJYE4AU.js.map +0 -1
  142. package/dist/chunk-EQRUAP2A.js.map +0 -1
  143. package/dist/chunk-RVJFGRGK.js.map +0 -1
  144. package/dist/chunk-RVU3RBE6.js.map +0 -1
  145. package/dist/chunk-WUV3PSCZ.js.map +0 -1
@@ -1,8 +1,10 @@
1
- import { foldMsgs } from './chunk-BNJYE4AU.js';
1
+ import { foldMsgs } from './chunk-QLA76GIT.js';
2
2
 
3
3
  // src/prediction/index.ts
4
4
  var tagSeq = (seq, value) => ({ seq, value });
5
5
  var ack = (lastAppliedSeq) => ({ lastAppliedSeq });
6
+ var NO_ACK = -1;
7
+ var initAck = () => ack(NO_ACK);
6
8
  var partitionByAck = (buffer, ackOrSeq) => {
7
9
  const lastAppliedSeq = typeof ackOrSeq === "number" ? ackOrSeq : ackOrSeq.lastAppliedSeq;
8
10
  const acked = [];
@@ -26,6 +28,6 @@ function reconcile(machine, authoritativeState, lastAppliedSeq, pending) {
26
28
  );
27
29
  }
28
30
 
29
- export { ack, nextSeq, partitionByAck, reconcile, tagSeq };
30
- //# sourceMappingURL=chunk-RVU3RBE6.js.map
31
- //# sourceMappingURL=chunk-RVU3RBE6.js.map
31
+ export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq };
32
+ //# sourceMappingURL=chunk-SQTCS44W.js.map
33
+ //# sourceMappingURL=chunk-SQTCS44W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prediction/index.ts"],"names":[],"mappings":";;;AAgDO,IAAM,SAAS,CAAI,GAAA,EAAU,KAAA,MAA4B,EAAE,KAAK,KAAA,EAAM;AActE,IAAM,GAAA,GAAM,CAAC,cAAA,MAA8B,EAAE,cAAA,EAAe;AAgB5D,IAAM,MAAA,GAAc;AAOpB,IAAM,OAAA,GAAU,MAAW,GAAA,CAAI,MAAM;AAmBrC,IAAM,cAAA,GAAiB,CAC5B,MAAA,EACA,QAAA,KACoB;AACpB,EAAA,MAAM,cAAA,GACJ,OAAO,QAAA,KAAa,QAAA,GAAW,WAAW,QAAA,CAAS,cAAA;AACrD,EAAA,MAAM,QAAwB,EAAC;AAC/B,EAAA,MAAM,UAA0B,EAAC;AACjC,EAAA,KAAA,MAAW,QAAQ,MAAA,EAAQ;AACzB,IAAA,IAAI,IAAA,CAAK,OAAO,cAAA,EAAgB;AAC9B,MAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AAAA,IACjB,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA,IACnB;AAAA,EACF;AACA,EAAA,OAAO,EAAE,OAAO,OAAA,EAAQ;AAC1B;AAQO,IAAM,OAAA,GAAU,CAAI,MAAA,KACzB,MAAA,CAAO,OAAO,CAAC,GAAA,EAAK,IAAA,KAAU,IAAA,CAAK,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,GAAM,GAAA,EAAM,EAAE,CAAA,GAAI;AAwBjE,SAAS,SAAA,CAOd,OAAA,EACA,kBAAA,EACA,cAAA,EACA,OAAA,EACG;AACH,EAAA,MAAM,EAAE,OAAA,EAAS,OAAA,EAAQ,GAAI,cAAA,CAAe,SAAS,cAAc,CAAA;AACnE,EAAA,OAAO,QAAA;AAAA,IACL,OAAA;AAAA,IACA,kBAAA;AAAA,IACA,OAAA,CAAQ,GAAA,CAAI,CAAC,MAAA,KAAW,OAAO,KAAK;AAAA,GACtC;AACF","file":"chunk-SQTCS44W.js","sourcesContent":["/**\n * @demlik/tea/prediction — the client-prediction ack primitive (epic #186, facet 2).\n *\n * The contract the Gambetta/Valve authoritative-server netcode loop needs:\n * **command-seq-in → last-applied-seq-out.** A client tags each predicted\n * command/`Msg` with a monotonic `seq`; the authoritative side replies with the\n * `lastAppliedSeq` it has processed. `partitionByAck` then splits the client's\n * pending buffer into the inputs the server has ACKED (`seq <= lastAppliedSeq`)\n * and those still PENDING (`seq > lastAppliedSeq`) — the un-acked tail the\n * reconciliation pass (#214) replays over the authoritative snapshot via\n * `foldMsgs` (#211, `@demlik/tea/pure`).\n *\n * Deliberately **Model-shape-agnostic** (ADR 0006): the ack is a standalone\n * value, NOT a field this module requires on the consumer's `State`. It\n * generalizes the `lastAckedSeq`-on-the-Model + `seq`-threaded-through-the-Msg\n * pattern vortex hand-rolled (epic #186). It is a LEAF module — pure and\n * runtime-free, importing nothing from the tea core or host, so it can never\n * drag `run`/the host into a client bundle.\n *\n * This is the read/ack side of the primitive; `tagSeq`/`nextSeq` are the write\n * side, letting a client mint monotonic seqs from its pending buffer alone —\n * again with no counter field on the Model.\n *\n * `reconcile` (the read side's payoff, #214) composes this ack partition with\n * the pure fold seam `foldMsgs` (#211) to perform the whole Gambetta/Valve\n * reconciliation step in one call. The `foldMsgs` import is the ONLY dependency\n * this leaf takes, and it reaches into the pure-core leaf (`../pure/core`) — NOT\n * the runtime root — so the module stays runtime-free and the `@demlik/tea/pure`\n * import-graph guard (ADR 0006, #213) still holds.\n */\n\nimport type { Cmd, Machine, Sub } from \"../pure/core\";\nimport { foldMsgs } from \"../pure/core\";\n\n/**\n * A monotonic, non-negative sequence number tagging one client-predicted\n * command. Monotonicity is the client's contract (mint successive seqs with\n * `nextSeq`); this module only ever *compares* seqs.\n */\nexport type Seq = number;\n\n/** A command (or `Msg`) tagged with the `seq` the client assigned it. */\nexport interface SeqTagged<T> {\n readonly seq: Seq;\n readonly value: T;\n}\n\n/** Tag a command/`Msg` with its sequence number. */\nexport const tagSeq = <T>(seq: Seq, value: T): SeqTagged<T> => ({ seq, value });\n\n/**\n * The authoritative side's acknowledgement: the highest `seq` it has applied.\n * Everything with `seq <= lastAppliedSeq` is settled; everything above is still\n * in flight. This *is* the whole \"ack from the authoritative side\" — a value\n * the consumer carries however it likes (a wire field, a message payload),\n * never a shape this module imposes on the Model.\n */\nexport interface Ack {\n readonly lastAppliedSeq: Seq;\n}\n\n/** Construct an `Ack` for a last-applied sequence number. */\nexport const ack = (lastAppliedSeq: Seq): Ack => ({ lastAppliedSeq });\n\n/**\n * The \"nothing applied yet\" cursor — the ack value for a server that has applied\n * no client input at all. Because `nextSeq` mints **0-based** seqs and\n * `partitionByAck` acks **inclusively** (`seq <= lastAppliedSeq`), the pre-first\n * cursor must sit strictly *below* seq 0: `lastAppliedSeq = 0` would falsely ack\n * the seq-0 input before the server ever saw it. `-1` is \"before any seq\", and\n * `partitionByAck(buffer, NO_ACK)` therefore leaves the whole buffer PENDING.\n *\n * This names the boundary value on the seam so no consumer has to re-discover it\n * as a magic number: it is the primitive's own design (0-based mint + inclusive\n * partition) that creates the trap, so it is the primitive's job to name the\n * escape. Pair with {@link initAck} when an {@link Ack} is wanted instead of the\n * bare `Seq`.\n */\nexport const NO_ACK: Seq = -1;\n\n/**\n * The `Ack` for a server that has applied nothing yet — `ack(NO_ACK)`. The `Ack`\n * counterpart to the {@link NO_ACK} `Seq` sentinel, for a consumer that carries\n * an `Ack` rather than a bare `lastAppliedSeq`.\n */\nexport const initAck = (): Ack => ack(NO_ACK);\n\n/** The result of splitting a seq-tagged buffer against an ack. */\nexport interface AckPartition<T> {\n /** Inputs the authoritative side has applied (`seq <= lastAppliedSeq`). */\n readonly acked: readonly SeqTagged<T>[];\n /** Inputs still in flight / un-acked (`seq > lastAppliedSeq`) — the replay tail. */\n readonly pending: readonly SeqTagged<T>[];\n}\n\n/**\n * Partition a buffer of seq-tagged commands into ACKED and PENDING against the\n * authoritative `lastAppliedSeq`. Pure and total: every item lands in exactly\n * one bucket, input order is preserved within each bucket, and the boundary\n * `seq == lastAppliedSeq` is ACKED (inclusive `<=`). Routing is by `seq` value,\n * not buffer position, so an out-of-order or duplicate-seq buffer partitions\n * correctly. Accepts either an {@link Ack} or a bare `lastAppliedSeq` number, so\n * a caller passes whichever it holds.\n */\nexport const partitionByAck = <T>(\n buffer: readonly SeqTagged<T>[],\n ackOrSeq: Ack | Seq,\n): AckPartition<T> => {\n const lastAppliedSeq =\n typeof ackOrSeq === \"number\" ? ackOrSeq : ackOrSeq.lastAppliedSeq;\n const acked: SeqTagged<T>[] = [];\n const pending: SeqTagged<T>[] = [];\n for (const item of buffer) {\n if (item.seq <= lastAppliedSeq) {\n acked.push(item);\n } else {\n pending.push(item);\n }\n }\n return { acked, pending };\n};\n\n/**\n * The next sequence number to assign: one past the highest `seq` in the buffer,\n * or `0` for an empty buffer. Lets a client mint monotonic seqs from its pending\n * buffer alone — no counter field on the Model — and is tolerant of an unordered\n * buffer (it takes the max, not the last element).\n */\nexport const nextSeq = <T>(buffer: readonly SeqTagged<T>[]): Seq =>\n buffer.reduce((max, item) => (item.seq > max ? item.seq : max), -1) + 1;\n\n/**\n * The client prediction/reconciliation helper — the Gambetta/Valve\n * authoritative-server loop's reconcile step, generalized (#214, epic #186).\n *\n * Given the latest `authoritativeState` from the server, the server's\n * `lastAppliedSeq` ack, and the client's `pending` buffer of seq-tagged inputs,\n * it returns the corrected predicted state: drop the inputs the server has\n * already applied (`seq <= lastAppliedSeq`) and replay ONLY the un-acked tail\n * (`seq > lastAppliedSeq`) over the authoritative snapshot.\n *\n * It is **composed, not re-derived** (ADR 0006): the drop is {@link partitionByAck}\n * (the ack primitive, #212) and the replay is `foldMsgs` (the pure fold seam,\n * #211) — there is no second copy of the partition logic and no second copy of\n * the fold. `lastAppliedSeq` accepts either an {@link Ack} or a bare `Seq`, so a\n * caller passes whichever it holds, matching `partitionByAck`.\n *\n * Pure and host-agnostic: it takes the `machine` (which `foldMsgs` needs to\n * dispatch `update`) but touches no `Store`, `interpret`, or subscription, and\n * imports nothing from the runtime — so it ships on `@demlik/tea/prediction`\n * and the client-safe `@demlik/tea/pure` umbrella without dragging `run` into a\n * client bundle.\n */\nexport function reconcile<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n authoritativeState: S,\n lastAppliedSeq: Ack | Seq,\n pending: readonly SeqTagged<M>[],\n): S {\n const { pending: unacked } = partitionByAck(pending, lastAppliedSeq);\n return foldMsgs(\n machine,\n authoritativeState,\n unacked.map((tagged) => tagged.value),\n );\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { createRaftNode } from './chunk-XRXKH7KA.js';
1
+ import { createRaftNode } from './chunk-6RGBKODV.js';
2
2
 
3
3
  // src/raft/sim.ts
4
4
  function clusterConfigs(n, over) {
@@ -198,5 +198,5 @@ function clusterStatesEqual(a, b) {
198
198
  }
199
199
 
200
200
  export { clusterConfigs, clusterStatesEqual, replaySchedule, runSchedule };
201
- //# sourceMappingURL=chunk-7VEG4KHB.js.map
202
- //# sourceMappingURL=chunk-7VEG4KHB.js.map
201
+ //# sourceMappingURL=chunk-VLGARN6H.js.map
202
+ //# sourceMappingURL=chunk-VLGARN6H.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/raft/sim.ts"],"names":[],"mappings":"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","file":"chunk-7VEG4KHB.js","sourcesContent":["/**\n * @demlik/tea/raft — a deterministic, in-memory multi-node Raft simulation\n * driver. Slice #121 (the safety-property half) of the consensus epic (#117):\n * the headline payoff is consensus verified by **deterministic replay**, not by\n * flaky wall-clock timing.\n *\n * This module owns NO wall clock, NO real timers, and NO networking. Time and\n * message ordering are entirely the *schedule* — a finite list of {@link SimEvent}s\n * the caller (a fast-check generator or an explicit fixture) hands the driver.\n * The driver folds the schedule one event at a time over a cluster of pure\n * {@link createRaftNode}s, routing every emitted `Cmd` into a pending-message\n * pool and recording the full `(event → resulting cluster states)` trace.\n *\n * Because the underlying reducer is pure (no ambient clock — `at` rides on the\n * Msg; no ambient RNG — `rng` injected once per node), the same schedule re-run\n * yields **byte-identical** cluster state. That replay identity is the property\n * the safety suite (`safety.test.ts`) leans on: a violated invariant shrinks to\n * a *replayable schedule*, not an irreproducible timing fluke (errors are data).\n *\n * ## The schedule model\n *\n * The cluster is N nodes plus a pending-message pool. Each {@link SimEvent} is\n * one of two moves:\n *\n * - `{ kind: \"timer\", node, timer }` — fire node's election OR heartbeat timer.\n * Drives an `onElectionTimeout` / `onHeartbeat` verb on that node. The\n * firing `at` is the driver's monotonic logical clock (one tick per event),\n * so no verb reads a real clock.\n * - `{ kind: \"deliver\", index }` — deliver the in-flight message at `index` in\n * the pending pool to its target node, as the matching inbound `RaftMsg`.\n * An out-of-range `index` is taken modulo the pool size (so a fast-check\n * integer always names *some* deliverable message when the pool is\n * non-empty); a `deliver` against an empty pool is a no-op event (recorded,\n * advances the clock, changes nothing) — the schedule stays total.\n *\n * Delivering a message removes it from the pool and feeds it to the target's\n * verb; any Cmds that verb emits are appended to the pool (each Cmd carries its\n * own `to`, so routing is just a lookup). This is the in-memory transport: a\n * `SendRequestVote{to}` delivered becomes a `RequestVoteRequest` on `to`; the\n * `SendRequestVoteReply{to}` it emits becomes a `RequestVoteReply` on the\n * original candidate; and so on around the RPC pairs.\n *\n * ## Record / replay\n *\n * {@link runSchedule} returns a {@link SimTrace}: the ordered list of steps, each\n * the event applied and the resulting per-node {@link RaftState} snapshot, plus\n * the final cluster. {@link replaySchedule} re-runs the identical schedule from a\n * fresh cluster; {@link clusterStatesEqual} compares two cluster snapshots\n * structurally. The safety suite asserts `runSchedule(s)` and\n * `replaySchedule(s)` agree byte-for-byte.\n *\n * NOT a substrate primitive and NOT shipped in the node's public verb surface:\n * it depends only on `./index` (the pure node) read-only, and lives beside the\n * node as a test/replay harness.\n */\n\nimport {\n type AppendEntriesReply,\n type AppendEntriesRequest,\n createRaftNode,\n type LogEntry,\n type NodeId,\n type RaftCmd,\n type RaftConfig,\n type RaftMsg,\n type RaftNode,\n type RaftState,\n type RequestVoteReply,\n type RequestVoteRequest,\n} from \"./index\";\n\n// ===========================================================================\n// Schedule — the controllable, wall-clock-free event stream\n// ===========================================================================\n\n/** Which of a node's two timers a `timer` event fires. */\nexport type TimerKind = \"election\" | \"heartbeat\";\n\n/**\n * One move in a schedule. The whole simulation is a fold of these — there is no\n * other source of time or ordering. `_tag`-style `kind` discriminant matches the\n * engine's tagged-union convention.\n */\nexport type SimEvent =\n | {\n /** Fire a node's timer (drives `onElectionTimeout` / `onHeartbeat`). */\n readonly kind: \"timer\";\n /** The node whose timer fires. */\n readonly node: NodeId;\n /** Which timer. */\n readonly timer: TimerKind;\n }\n | {\n /**\n * Deliver an in-flight message from the pending pool to its target. The\n * index is taken modulo the pool size, so any integer names a deliverable\n * message when the pool is non-empty; a deliver against an empty pool is a\n * recorded no-op (the clock still advances).\n */\n readonly kind: \"deliver\";\n /** Which pending message to deliver (modulo pool size). */\n readonly index: number;\n }\n | {\n /**\n * A client submits a command to a node (Figure 2 §5.3). On the leader it\n * appends + replicates; on a non-leader it is a no-op. This is how the\n * replicated log grows in a simulation.\n */\n readonly kind: \"client\";\n /** The node the client contacts. */\n readonly node: NodeId;\n /** The opaque command to submit. */\n readonly command: number;\n }\n | {\n /**\n * Drain the pending pool to quiescence: deliver messages FIFO (each new\n * Cmd a delivery emits is appended and itself delivered) until the pool is\n * empty or `bound` deliveries have run. This is the \"let the network\n * settle\" move — it lets a started election or a submitted client command\n * run its RPC round(s) to completion, which is what actually produces\n * leaders and committed entries. Without it, a purely-random `deliver`\n * index almost never threads the exact sequence an election + replication\n * needs (measured: 0 commits in 2000 random schedules vs. 529 with settle),\n * so the safety properties would pass *vacuously* over empty logs.\n *\n * The `bound` caps the drain so a pathological re-emit loop cannot diverge;\n * every step is still recorded (one trace step per delivery), so a settle\n * is a deterministic, replayable macro — not a black box.\n */\n readonly kind: \"settle\";\n /** Max deliveries to run (the drain is FIFO; stops early when empty). */\n readonly bound: number;\n }\n | {\n /**\n * Set the cluster's *partitioned* node set (a network fault, NOT a state\n * change). A node in `down` is isolated: the transport DROPS every message\n * to OR from it (both directions, as a real partition does) while its\n * timers still fire — so a crashed/partitioned leader stops being able to\n * heartbeat and the surviving majority elects a new leader. This is the\n * \"kill the leader\" / \"heal the partition\" move the failover demo needs.\n *\n * It names the WHOLE down-set absolutely (not a toggle): `{ down: [n0] }`\n * isolates n0; a later `{ down: [] }` heals every partition. Recorded as\n * one step (clock advances, no message moves), so it replays deterministically.\n */\n readonly kind: \"partition\";\n /** The nodes that are currently isolated (drop all their traffic). */\n readonly down: readonly NodeId[];\n };\n\n/** A whole schedule: the finite event stream the driver folds. */\nexport type Schedule = readonly SimEvent[];\n\n// ===========================================================================\n// In-flight messages — emitted Cmds parked in the pending pool\n// ===========================================================================\n\n/**\n * A Cmd a node emitted, parked until a `deliver` event routes it. We keep the\n * raw {@link RaftCmd} (it already carries `to`) plus the sender, so a delivery\n * can build the correct inbound {@link RaftMsg} on the target. Commands over a\n * numeric payload (the simulation's `C = number`).\n */\nexport interface InFlight {\n /** The node that emitted this Cmd (the inbound Msg's `from` / sender). */\n readonly from: NodeId;\n /** The emitted Cmd, addressed to `cmd.to`. */\n readonly cmd: RaftCmd<number>;\n}\n\n// ===========================================================================\n// Cluster + trace\n// ===========================================================================\n\n/** A snapshot of every node's state, keyed by id. */\nexport type ClusterStates = Readonly<Record<NodeId, RaftState<number>>>;\n\n/**\n * A read-off of the message a `deliver` step actually pulled off the pending\n * pool — sender, target, and the {@link RaftCmd} discriminant (`cmd.type`). This\n * is a pure projection of what the fold *already did* (the InFlight that\n * {@link deliverOne} consumed), recorded so a renderer can draw the RPC flow.\n * It is NOT a new input and changes NO simulation behavior. Recorded even when\n * the packet is dropped by a partition (the RPC was still attempted), so the\n * arrow shows the intent while the greyed node shows the drop.\n */\nexport interface DeliveredMessage {\n /** The node that emitted the delivered Cmd (the inbound Msg's sender). */\n readonly from: NodeId;\n /** The node the Cmd was addressed to (`cmd.to`). */\n readonly to: NodeId;\n /** The delivered Cmd's discriminant (`RaftCmd[\"type\"]`). */\n readonly kind: RaftCmd<number>[\"type\"];\n}\n\n/** One recorded step: the event applied and the cluster state it produced. */\nexport interface SimStep {\n /** The event applied at this step. */\n readonly event: SimEvent;\n /** The pending-pool size AFTER applying the event. */\n readonly pending: number;\n /** Every node's state after the event. */\n readonly states: ClusterStates;\n /**\n * On a `deliver` step (incl. each delivery a `settle` drains), the message\n * that was actually pulled off the pool. ABSENT on every other step kind and\n * on a `deliver` against an empty pool — additive + backward-compatible, so a\n * trace recorded before this field still folds and renders identically (field\n * absent ⇒ no arrow). Purely a read-off of {@link deliverOne}'s work.\n */\n readonly delivered?: DeliveredMessage;\n}\n\n/**\n * The full record of a run: the per-node configs, the ordered steps, and the\n * final cluster. Replaying the same schedule reproduces this byte-for-byte.\n */\nexport interface SimTrace {\n /** The cluster's node ids, in config order. */\n readonly nodes: readonly NodeId[];\n /** One step per event in the schedule (in order). */\n readonly steps: readonly SimStep[];\n /** The final per-node states (== `steps.at(-1)?.states` when non-empty). */\n readonly final: ClusterStates;\n}\n\n// ===========================================================================\n// Building the cluster\n// ===========================================================================\n\n/**\n * Build N node configs sharing one election-timeout window + heartbeat. Each\n * node's `peers` is every OTHER id. Ids are `n0..n{N-1}`.\n */\nexport function clusterConfigs(\n n: number,\n over?: Partial<Omit<RaftConfig, \"self\" | \"peers\">>,\n): readonly RaftConfig[] {\n const ids = Array.from({ length: n }, (_, i) => `n${i}`);\n return ids.map((self) => ({\n self,\n peers: ids.filter((id) => id !== self),\n electionTimeout: over?.electionTimeout ?? { minMs: 150, maxMs: 300 },\n heartbeatMs: over?.heartbeatMs ?? 50,\n }));\n}\n\n/**\n * A fixed-jitter rng for a node, derived from its index so that distinct nodes\n * draw *distinct but deterministic* election timeouts (the real-cluster effect\n * that breaks split votes), while a replay with the same configs reproduces them\n * exactly. Pure function of the index — no ambient randomness.\n */\nfunction fixedRng(index: number, count: number): () => number {\n // Spread the [0,1) jitter evenly across the cluster: node i draws i/count.\n const value = count > 0 ? index / count : 0;\n return () => value;\n}\n\n// ===========================================================================\n// The driver\n// ===========================================================================\n\n/**\n * The mutable bag the fold threads. Kept local to {@link runSchedule}; never\n * escapes. `clock` is the monotonic logical clock — incremented once per event\n * and used as the `at` for timer firings, so the reducer never reads a real\n * clock and a replay reproduces every `at`.\n */\ninterface SimWorld {\n readonly handles: ReadonlyMap<NodeId, RaftNode<number>>;\n states: Record<NodeId, RaftState<number>>;\n pending: InFlight[];\n clock: number;\n /**\n * The currently-partitioned nodes (a `partition` event sets this absolutely).\n * The transport drops any in-flight message to OR from a down node — its\n * timers still fire, but its RPCs never land and it never hears the cluster.\n */\n down: Set<NodeId>;\n}\n\n/**\n * Translate one emitted {@link RaftCmd} into the inbound {@link RaftMsg} the\n * target node should receive. Pure mapping over the RPC pairs (Figure 2 wire\n * shapes); `sender` becomes the Msg's `from` / candidateId / leaderId as\n * appropriate. The target is always `cmd.to`.\n */\nfunction cmdToInbound(sender: NodeId, cmd: RaftCmd<number>): RaftMsg<number> {\n switch (cmd.type) {\n case \"raft:send_request_vote\": {\n const msg: RequestVoteRequest = {\n _tag: \"request_vote_request\",\n term: cmd.term,\n candidateId: cmd.candidateId,\n lastLogIndex: cmd.lastLogIndex,\n lastLogTerm: cmd.lastLogTerm,\n };\n return msg;\n }\n case \"raft:send_request_vote_reply\": {\n const msg: RequestVoteReply = {\n _tag: \"request_vote_reply\",\n term: cmd.term,\n from: sender,\n voteGranted: cmd.voteGranted,\n };\n return msg;\n }\n case \"raft:send_append_entries\": {\n const msg: AppendEntriesRequest<number> = {\n _tag: \"append_entries_request\",\n term: cmd.term,\n leaderId: cmd.leaderId,\n prevLogIndex: cmd.prevLogIndex,\n prevLogTerm: cmd.prevLogTerm,\n entries: cmd.entries,\n leaderCommit: cmd.leaderCommit,\n };\n return msg;\n }\n case \"raft:send_append_entries_reply\": {\n const msg: AppendEntriesReply = {\n _tag: \"append_entries_reply\",\n term: cmd.term,\n from: sender,\n success: cmd.success,\n matchIndex: cmd.matchIndex,\n };\n return msg;\n }\n }\n}\n\n/** Run one node verb for the inbound Msg, returning `[next, cmds]`. */\nfunction applyInbound(\n node: RaftNode<number>,\n state: RaftState<number>,\n msg: RaftMsg<number>,\n at: number,\n) {\n switch (msg._tag) {\n case \"election_timeout_fired\":\n return node.onElectionTimeout(state, at);\n case \"heartbeat_fired\":\n return node.onHeartbeat(state, at);\n case \"request_vote_request\":\n return node.onRequestVote(state, msg);\n case \"request_vote_reply\":\n return node.onRequestVoteReply(state, msg);\n case \"append_entries_request\":\n return node.onAppendEntries(state, msg);\n case \"append_entries_reply\":\n return node.onAppendEntriesReply(state, msg);\n case \"client_command\":\n return node.onClientCommand(state, msg);\n }\n}\n\n/** Park the Cmds an acting node emitted into the pending pool (FIFO append). */\nfunction park(\n world: SimWorld,\n actor: NodeId,\n emitted: readonly RaftCmd<number>[],\n): void {\n for (const cmd of emitted) {\n world.pending.push({ from: actor, cmd });\n }\n}\n\n/**\n * Deliver the pending message at `index` (modulo the pool size) to its target,\n * parking whatever it emits. Advances the clock and returns the {@link InFlight}\n * it consumed from the pool (so a caller can record what was delivered),\n * or `null` when the pool was empty (a recorded no-op). A single atomic unit of\n * simulated work — one trace step.\n *\n * The return value is a PURE read-off of work the function already does (the\n * splice was always there); returning it instead of a `boolean` adds no\n * behavior — every existing call-site path (apply, partition-drop, empty-pool)\n * is byte-for-byte unchanged.\n */\nfunction deliverOne(world: SimWorld, index: number): InFlight | null {\n if (world.pending.length === 0) {\n world.clock += 1;\n return null;\n }\n world.clock += 1;\n const at = world.clock;\n const i =\n ((index % world.pending.length) + world.pending.length) %\n world.pending.length;\n const [inFlight] = world.pending.splice(i, 1);\n if (!inFlight) return null;\n const target = inFlight.cmd.to;\n // Partition: a message to OR from an isolated node is dropped — consumed from\n // the pool (so a `settle` still terminates) but never applied (the packet was\n // lost on the wire). Both directions, as a real partition severs.\n if (world.down.has(target) || world.down.has(inFlight.from)) {\n return inFlight;\n }\n const node = world.handles.get(target);\n const state = world.states[target];\n if (node && state) {\n const inbound = cmdToInbound(inFlight.from, inFlight.cmd);\n const [next, cmds] = applyInbound(node, state, inbound, at);\n world.states[target] = next;\n park(world, target, cmds);\n }\n return inFlight;\n}\n\n/** Project a consumed {@link InFlight} into the additive {@link DeliveredMessage} read-off. */\nfunction deliveredOf(inFlight: InFlight): DeliveredMessage {\n return { from: inFlight.from, to: inFlight.cmd.to, kind: inFlight.cmd.type };\n}\n\n/**\n * Apply one {@link SimEvent} to the world, pushing one trace step PER unit of\n * work onto `steps`. A `timer` / `client` / `deliver` produces exactly one step;\n * a `settle` produces one step per drained delivery (so the macro stays a\n * transparent, replayable sequence — not a black box). Mutates the world's\n * `states` / `pending` / `clock` in place (the local fold bag).\n */\nfunction applyEvent(world: SimWorld, event: SimEvent, steps: SimStep[]): void {\n if (event.kind === \"partition\") {\n // A pure network-fault move: replace the down-set, advance the clock, record\n // a step. No message moves and no node state changes — the effect surfaces\n // later when `deliverOne` drops traffic to/from a down node.\n world.clock += 1;\n world.down = new Set(event.down);\n steps.push({\n event,\n pending: world.pending.length,\n states: snapshot(world.states),\n });\n return;\n }\n\n if (event.kind === \"settle\") {\n // Drain FIFO (index 0) until empty or the bound is hit. Each delivery is its\n // own recorded step, tagged as the deliver-index-0 it actually is.\n const bound = Math.max(0, event.bound);\n for (let n = 0; n < bound && world.pending.length > 0; n++) {\n const inFlight = deliverOne(world, 0);\n steps.push({\n event: { kind: \"deliver\", index: 0 },\n pending: world.pending.length,\n states: snapshot(world.states),\n // Each drained delivery is its OWN step, so record that step's own\n // delivered message (not \"the last of the settle\"). The pool was\n // non-empty (loop guard), so `inFlight` is never null here.\n ...(inFlight ? { delivered: deliveredOf(inFlight) } : {}),\n });\n }\n return;\n }\n\n // The message a top-level `deliver` consumed (null on an empty-pool no-op);\n // read off below into the step's additive `delivered` field. `undefined` for\n // every non-deliver event kind.\n let delivered: InFlight | null = null;\n if (event.kind === \"deliver\") {\n delivered = deliverOne(world, event.index);\n } else {\n // timer / client: drive the acting node's verb directly.\n world.clock += 1;\n const at = world.clock;\n const actor = event.node;\n const node = world.handles.get(actor);\n const state = world.states[actor];\n if (node && state) {\n const inbound: RaftMsg<number> =\n event.kind === \"timer\"\n ? event.timer === \"election\"\n ? { _tag: \"election_timeout_fired\", at }\n : { _tag: \"heartbeat_fired\", at }\n : { _tag: \"client_command\", command: event.command };\n const [next, cmds] = applyInbound(node, state, inbound, at);\n world.states[actor] = next;\n park(world, actor, cmds);\n }\n }\n\n steps.push({\n event,\n pending: world.pending.length,\n states: snapshot(world.states),\n // Additive read-off: present only when a `deliver` actually consumed a\n // message. Absent (key omitted) for timer/client and empty-pool delivers,\n // so the step is structurally identical to a pre-#144 trace.\n ...(delivered ? { delivered: deliveredOf(delivered) } : {}),\n });\n}\n\n/** Deep-freeze-free structural copy of a cluster snapshot (states are already immutable values). */\nfunction snapshot(states: Record<NodeId, RaftState<number>>): ClusterStates {\n return { ...states };\n}\n\n/**\n * Run a schedule against a fresh N-node cluster, recording the full trace. Pure\n * given `(configs, schedule)`: no clock, no RNG, no IO — the only \"time\" is the\n * driver's logical clock, advanced one tick per event. Each node gets a\n * fixed-jitter rng derived from its index (distinct but deterministic timeouts).\n */\nexport function runSchedule(\n configs: readonly RaftConfig[],\n schedule: Schedule,\n): SimTrace {\n const handles = new Map<NodeId, RaftNode<number>>();\n const states: Record<NodeId, RaftState<number>> = {};\n configs.forEach((config, i) => {\n const node = createRaftNode<number>(config, fixedRng(i, configs.length));\n handles.set(config.self, node);\n states[config.self] = node.init();\n });\n\n const world: SimWorld = {\n handles,\n states,\n pending: [],\n clock: 0,\n down: new Set(),\n };\n const steps: SimStep[] = [];\n\n for (const event of schedule) {\n applyEvent(world, event, steps);\n }\n\n return {\n nodes: configs.map((c) => c.self),\n steps,\n final: snapshot(world.states),\n };\n}\n\n/**\n * Replay a schedule from a fresh cluster. Identical to {@link runSchedule} (it\n * IS `runSchedule`) — exposed under a name that documents intent at the call\n * site: a replay is meant to reproduce a prior run's trace byte-for-byte.\n */\nexport function replaySchedule(\n configs: readonly RaftConfig[],\n schedule: Schedule,\n): SimTrace {\n return runSchedule(configs, schedule);\n}\n\n// ===========================================================================\n// Comparison helpers (for the determinism property + counterexample reports)\n// ===========================================================================\n\n/**\n * Structural equality of two cluster snapshots. Used by the determinism\n * property: a run and its replay must produce identical per-node state for every\n * node. (Tests use vitest `toEqual` for the same comparison; this is the\n * dependency-free version the driver can expose for ad-hoc checks.)\n */\nexport function clusterStatesEqual(\n a: ClusterStates,\n b: ClusterStates,\n): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n// ===========================================================================\n// Re-exports the safety suite reads (so it imports the harness, not ./index)\n// ===========================================================================\n\nexport type { LogEntry, NodeId, RaftConfig, RaftState };\n"]}
1
+ {"version":3,"sources":["../src/raft/sim.ts"],"names":[],"mappings":"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","file":"chunk-VLGARN6H.js","sourcesContent":["/**\n * @demlik/tea/raft — a deterministic, in-memory multi-node Raft simulation\n * driver. Slice #121 (the safety-property half) of the consensus epic (#117):\n * the headline payoff is consensus verified by **deterministic replay**, not by\n * flaky wall-clock timing.\n *\n * This module owns NO wall clock, NO real timers, and NO networking. Time and\n * message ordering are entirely the *schedule* — a finite list of {@link SimEvent}s\n * the caller (a fast-check generator or an explicit fixture) hands the driver.\n * The driver folds the schedule one event at a time over a cluster of pure\n * {@link createRaftNode}s, routing every emitted `Cmd` into a pending-message\n * pool and recording the full `(event → resulting cluster states)` trace.\n *\n * Because the underlying reducer is pure (no ambient clock — `at` rides on the\n * Msg; no ambient RNG — `rng` injected once per node), the same schedule re-run\n * yields **byte-identical** cluster state. That replay identity is the property\n * the safety suite (`safety.test.ts`) leans on: a violated invariant shrinks to\n * a *replayable schedule*, not an irreproducible timing fluke (errors are data).\n *\n * ## The schedule model\n *\n * The cluster is N nodes plus a pending-message pool. Each {@link SimEvent} is\n * one of two moves:\n *\n * - `{ kind: \"timer\", node, timer }` — fire node's election OR heartbeat timer.\n * Drives an `onElectionTimeout` / `onHeartbeat` verb on that node. The\n * firing `at` is the driver's monotonic logical clock (one tick per event),\n * so no verb reads a real clock.\n * - `{ kind: \"deliver\", index }` — deliver the in-flight message at `index` in\n * the pending pool to its target node, as the matching inbound `RaftMsg`.\n * An out-of-range `index` is taken modulo the pool size (so a fast-check\n * integer always names *some* deliverable message when the pool is\n * non-empty); a `deliver` against an empty pool is a no-op event (recorded,\n * advances the clock, changes nothing) — the schedule stays total.\n *\n * Delivering a message removes it from the pool and feeds it to the target's\n * verb; any Cmds that verb emits are appended to the pool (each Cmd carries its\n * own `to`, so routing is just a lookup). This is the in-memory transport: a\n * `SendRequestVote{to}` delivered becomes a `RequestVoteRequest` on `to`; the\n * `SendRequestVoteReply{to}` it emits becomes a `RequestVoteReply` on the\n * original candidate; and so on around the RPC pairs.\n *\n * ## Record / replay\n *\n * {@link runSchedule} returns a {@link SimTrace}: the ordered list of steps, each\n * the event applied and the resulting per-node {@link RaftState} snapshot, plus\n * the final cluster. {@link replaySchedule} re-runs the identical schedule from a\n * fresh cluster; {@link clusterStatesEqual} compares two cluster snapshots\n * structurally. The safety suite asserts `runSchedule(s)` and\n * `replaySchedule(s)` agree byte-for-byte.\n *\n * NOT a substrate primitive and NOT shipped in the node's public verb surface:\n * it depends only on `./index` (the pure node) read-only, and lives beside the\n * node as a test/replay harness.\n */\n\nimport {\n type AppendEntriesReply,\n type AppendEntriesRequest,\n createRaftNode,\n type LogEntry,\n type NodeId,\n type RaftCmd,\n type RaftConfig,\n type RaftMsg,\n type RaftNode,\n type RaftState,\n type RequestVoteReply,\n type RequestVoteRequest,\n} from \"./index\";\n\n// ===========================================================================\n// Schedule — the controllable, wall-clock-free event stream\n// ===========================================================================\n\n/** Which of a node's two timers a `timer` event fires. */\nexport type TimerKind = \"election\" | \"heartbeat\";\n\n/**\n * One move in a schedule. The whole simulation is a fold of these — there is no\n * other source of time or ordering. `_tag`-style `kind` discriminant matches the\n * engine's tagged-union convention.\n */\nexport type SimEvent =\n | {\n /** Fire a node's timer (drives `onElectionTimeout` / `onHeartbeat`). */\n readonly kind: \"timer\";\n /** The node whose timer fires. */\n readonly node: NodeId;\n /** Which timer. */\n readonly timer: TimerKind;\n }\n | {\n /**\n * Deliver an in-flight message from the pending pool to its target. The\n * index is taken modulo the pool size, so any integer names a deliverable\n * message when the pool is non-empty; a deliver against an empty pool is a\n * recorded no-op (the clock still advances).\n */\n readonly kind: \"deliver\";\n /** Which pending message to deliver (modulo pool size). */\n readonly index: number;\n }\n | {\n /**\n * A client submits a command to a node (Figure 2 §5.3). On the leader it\n * appends + replicates; on a non-leader it is a no-op. This is how the\n * replicated log grows in a simulation.\n */\n readonly kind: \"client\";\n /** The node the client contacts. */\n readonly node: NodeId;\n /** The opaque command to submit. */\n readonly command: number;\n }\n | {\n /**\n * Drain the pending pool to quiescence: deliver messages FIFO (each new\n * Cmd a delivery emits is appended and itself delivered) until the pool is\n * empty or `bound` deliveries have run. This is the \"let the network\n * settle\" move — it lets a started election or a submitted client command\n * run its RPC round(s) to completion, which is what actually produces\n * leaders and committed entries. Without it, a purely-random `deliver`\n * index almost never threads the exact sequence an election + replication\n * needs (measured: 0 commits in 2000 random schedules vs. 529 with settle),\n * so the safety properties would pass *vacuously* over empty logs.\n *\n * The `bound` caps the drain so a pathological re-emit loop cannot diverge;\n * every step is still recorded (one trace step per delivery), so a settle\n * is a deterministic, replayable macro — not a black box.\n */\n readonly kind: \"settle\";\n /** Max deliveries to run (the drain is FIFO; stops early when empty). */\n readonly bound: number;\n }\n | {\n /**\n * Set the cluster's *partitioned* node set (a network fault, NOT a state\n * change). A node in `down` is isolated: the transport DROPS every message\n * to OR from it (both directions, as a real partition does) while its\n * timers still fire — so a crashed/partitioned leader stops being able to\n * heartbeat and the surviving majority elects a new leader. This is the\n * \"kill the leader\" / \"heal the partition\" move the failover demo needs.\n *\n * It names the WHOLE down-set absolutely (not a toggle): `{ down: [n0] }`\n * isolates n0; a later `{ down: [] }` heals every partition. Recorded as\n * one step (clock advances, no message moves), so it replays deterministically.\n */\n readonly kind: \"partition\";\n /** The nodes that are currently isolated (drop all their traffic). */\n readonly down: readonly NodeId[];\n };\n\n/** A whole schedule: the finite event stream the driver folds. */\nexport type Schedule = readonly SimEvent[];\n\n// ===========================================================================\n// In-flight messages — emitted Cmds parked in the pending pool\n// ===========================================================================\n\n/**\n * A Cmd a node emitted, parked until a `deliver` event routes it. We keep the\n * raw {@link RaftCmd} (it already carries `to`) plus the sender, so a delivery\n * can build the correct inbound {@link RaftMsg} on the target. Commands over a\n * numeric payload (the simulation's `C = number`).\n */\nexport interface InFlight {\n /** The node that emitted this Cmd (the inbound Msg's `from` / sender). */\n readonly from: NodeId;\n /** The emitted Cmd, addressed to `cmd.to`. */\n readonly cmd: RaftCmd<number>;\n}\n\n// ===========================================================================\n// Cluster + trace\n// ===========================================================================\n\n/** A snapshot of every node's state, keyed by id. */\nexport type ClusterStates = Readonly<Record<NodeId, RaftState<number>>>;\n\n/**\n * A read-off of the message a `deliver` step actually pulled off the pending\n * pool — sender, target, and the {@link RaftCmd} discriminant (`cmd.type`). This\n * is a pure projection of what the fold *already did* (the InFlight that\n * {@link deliverOne} consumed), recorded so a renderer can draw the RPC flow.\n * It is NOT a new input and changes NO simulation behavior. Recorded even when\n * the packet is dropped by a partition (the RPC was still attempted), so the\n * arrow shows the intent while the greyed node shows the drop.\n */\nexport interface DeliveredMessage {\n /** The node that emitted the delivered Cmd (the inbound Msg's sender). */\n readonly from: NodeId;\n /** The node the Cmd was addressed to (`cmd.to`). */\n readonly to: NodeId;\n /** The delivered Cmd's discriminant (`RaftCmd[\"type\"]`). */\n readonly kind: RaftCmd<number>[\"type\"];\n}\n\n/** One recorded step: the event applied and the cluster state it produced. */\nexport interface SimStep {\n /** The event applied at this step. */\n readonly event: SimEvent;\n /** The pending-pool size AFTER applying the event. */\n readonly pending: number;\n /** Every node's state after the event. */\n readonly states: ClusterStates;\n /**\n * On a `deliver` step (incl. each delivery a `settle` drains), the message\n * that was actually pulled off the pool. ABSENT on every other step kind and\n * on a `deliver` against an empty pool — additive + backward-compatible, so a\n * trace recorded before this field still folds and renders identically (field\n * absent ⇒ no arrow). Purely a read-off of {@link deliverOne}'s work.\n */\n readonly delivered?: DeliveredMessage;\n}\n\n/**\n * The full record of a run: the per-node configs, the ordered steps, and the\n * final cluster. Replaying the same schedule reproduces this byte-for-byte.\n */\nexport interface SimTrace {\n /** The cluster's node ids, in config order. */\n readonly nodes: readonly NodeId[];\n /** One step per event in the schedule (in order). */\n readonly steps: readonly SimStep[];\n /** The final per-node states (== `steps.at(-1)?.states` when non-empty). */\n readonly final: ClusterStates;\n}\n\n// ===========================================================================\n// Building the cluster\n// ===========================================================================\n\n/**\n * Build N node configs sharing one election-timeout window + heartbeat. Each\n * node's `peers` is every OTHER id. Ids are `n0..n{N-1}`.\n */\nexport function clusterConfigs(\n n: number,\n over?: Partial<Omit<RaftConfig, \"self\" | \"peers\">>,\n): readonly RaftConfig[] {\n const ids = Array.from({ length: n }, (_, i) => `n${i}`);\n return ids.map((self) => ({\n self,\n peers: ids.filter((id) => id !== self),\n electionTimeout: over?.electionTimeout ?? { minMs: 150, maxMs: 300 },\n heartbeatMs: over?.heartbeatMs ?? 50,\n }));\n}\n\n/**\n * A fixed-jitter rng for a node, derived from its index so that distinct nodes\n * draw *distinct but deterministic* election timeouts (the real-cluster effect\n * that breaks split votes), while a replay with the same configs reproduces them\n * exactly. Pure function of the index — no ambient randomness.\n */\nfunction fixedRng(index: number, count: number): () => number {\n // Spread the [0,1) jitter evenly across the cluster: node i draws i/count.\n const value = count > 0 ? index / count : 0;\n return () => value;\n}\n\n// ===========================================================================\n// The driver\n// ===========================================================================\n\n/**\n * The mutable bag the fold threads. Kept local to {@link runSchedule}; never\n * escapes. `clock` is the monotonic logical clock — incremented once per event\n * and used as the `at` for timer firings, so the reducer never reads a real\n * clock and a replay reproduces every `at`.\n */\ninterface SimWorld {\n readonly handles: ReadonlyMap<NodeId, RaftNode<number>>;\n states: Record<NodeId, RaftState<number>>;\n pending: InFlight[];\n clock: number;\n /**\n * The currently-partitioned nodes (a `partition` event sets this absolutely).\n * The transport drops any in-flight message to OR from a down node — its\n * timers still fire, but its RPCs never land and it never hears the cluster.\n */\n down: Set<NodeId>;\n}\n\n/**\n * Translate one emitted {@link RaftCmd} into the inbound {@link RaftMsg} the\n * target node should receive. Pure mapping over the RPC pairs (Figure 2 wire\n * shapes); `sender` becomes the Msg's `from` / candidateId / leaderId as\n * appropriate. The target is always `cmd.to`.\n */\nfunction cmdToInbound(sender: NodeId, cmd: RaftCmd<number>): RaftMsg<number> {\n switch (cmd.type) {\n case \"raft:send_request_vote\": {\n const msg: RequestVoteRequest = {\n _tag: \"request_vote_request\",\n term: cmd.term,\n candidateId: cmd.candidateId,\n lastLogIndex: cmd.lastLogIndex,\n lastLogTerm: cmd.lastLogTerm,\n };\n return msg;\n }\n case \"raft:send_request_vote_reply\": {\n const msg: RequestVoteReply = {\n _tag: \"request_vote_reply\",\n term: cmd.term,\n from: sender,\n voteGranted: cmd.voteGranted,\n };\n return msg;\n }\n case \"raft:send_append_entries\": {\n const msg: AppendEntriesRequest<number> = {\n _tag: \"append_entries_request\",\n term: cmd.term,\n leaderId: cmd.leaderId,\n prevLogIndex: cmd.prevLogIndex,\n prevLogTerm: cmd.prevLogTerm,\n entries: cmd.entries,\n leaderCommit: cmd.leaderCommit,\n };\n return msg;\n }\n case \"raft:send_append_entries_reply\": {\n const msg: AppendEntriesReply = {\n _tag: \"append_entries_reply\",\n term: cmd.term,\n from: sender,\n success: cmd.success,\n matchIndex: cmd.matchIndex,\n };\n return msg;\n }\n }\n}\n\n/** Run one node verb for the inbound Msg, returning `[next, cmds]`. */\nfunction applyInbound(\n node: RaftNode<number>,\n state: RaftState<number>,\n msg: RaftMsg<number>,\n at: number,\n) {\n switch (msg._tag) {\n case \"election_timeout_fired\":\n return node.onElectionTimeout(state, at);\n case \"heartbeat_fired\":\n return node.onHeartbeat(state, at);\n case \"request_vote_request\":\n return node.onRequestVote(state, msg);\n case \"request_vote_reply\":\n return node.onRequestVoteReply(state, msg);\n case \"append_entries_request\":\n return node.onAppendEntries(state, msg);\n case \"append_entries_reply\":\n return node.onAppendEntriesReply(state, msg);\n case \"client_command\":\n return node.onClientCommand(state, msg);\n }\n}\n\n/** Park the Cmds an acting node emitted into the pending pool (FIFO append). */\nfunction park(\n world: SimWorld,\n actor: NodeId,\n emitted: readonly RaftCmd<number>[],\n): void {\n for (const cmd of emitted) {\n world.pending.push({ from: actor, cmd });\n }\n}\n\n/**\n * Deliver the pending message at `index` (modulo the pool size) to its target,\n * parking whatever it emits. Advances the clock and returns the {@link InFlight}\n * it consumed from the pool (so a caller can record what was delivered),\n * or `null` when the pool was empty (a recorded no-op). A single atomic unit of\n * simulated work — one trace step.\n *\n * The return value is a PURE read-off of work the function already does (the\n * splice was always there); returning it instead of a `boolean` adds no\n * behavior — every existing call-site path (apply, partition-drop, empty-pool)\n * is byte-for-byte unchanged.\n */\nfunction deliverOne(world: SimWorld, index: number): InFlight | null {\n if (world.pending.length === 0) {\n world.clock += 1;\n return null;\n }\n world.clock += 1;\n const at = world.clock;\n const i =\n ((index % world.pending.length) + world.pending.length) %\n world.pending.length;\n const [inFlight] = world.pending.splice(i, 1);\n if (!inFlight) return null;\n const target = inFlight.cmd.to;\n // Partition: a message to OR from an isolated node is dropped — consumed from\n // the pool (so a `settle` still terminates) but never applied (the packet was\n // lost on the wire). Both directions, as a real partition severs.\n if (world.down.has(target) || world.down.has(inFlight.from)) {\n return inFlight;\n }\n const node = world.handles.get(target);\n const state = world.states[target];\n if (node && state) {\n const inbound = cmdToInbound(inFlight.from, inFlight.cmd);\n const [next, cmds] = applyInbound(node, state, inbound, at);\n world.states[target] = next;\n park(world, target, cmds);\n }\n return inFlight;\n}\n\n/** Project a consumed {@link InFlight} into the additive {@link DeliveredMessage} read-off. */\nfunction deliveredOf(inFlight: InFlight): DeliveredMessage {\n return { from: inFlight.from, to: inFlight.cmd.to, kind: inFlight.cmd.type };\n}\n\n/**\n * Apply one {@link SimEvent} to the world, pushing one trace step PER unit of\n * work onto `steps`. A `timer` / `client` / `deliver` produces exactly one step;\n * a `settle` produces one step per drained delivery (so the macro stays a\n * transparent, replayable sequence — not a black box). Mutates the world's\n * `states` / `pending` / `clock` in place (the local fold bag).\n */\nfunction applyEvent(world: SimWorld, event: SimEvent, steps: SimStep[]): void {\n if (event.kind === \"partition\") {\n // A pure network-fault move: replace the down-set, advance the clock, record\n // a step. No message moves and no node state changes — the effect surfaces\n // later when `deliverOne` drops traffic to/from a down node.\n world.clock += 1;\n world.down = new Set(event.down);\n steps.push({\n event,\n pending: world.pending.length,\n states: snapshot(world.states),\n });\n return;\n }\n\n if (event.kind === \"settle\") {\n // Drain FIFO (index 0) until empty or the bound is hit. Each delivery is its\n // own recorded step, tagged as the deliver-index-0 it actually is.\n const bound = Math.max(0, event.bound);\n for (let n = 0; n < bound && world.pending.length > 0; n++) {\n const inFlight = deliverOne(world, 0);\n steps.push({\n event: { kind: \"deliver\", index: 0 },\n pending: world.pending.length,\n states: snapshot(world.states),\n // Each drained delivery is its OWN step, so record that step's own\n // delivered message (not \"the last of the settle\"). The pool was\n // non-empty (loop guard), so `inFlight` is never null here.\n ...(inFlight ? { delivered: deliveredOf(inFlight) } : {}),\n });\n }\n return;\n }\n\n // The message a top-level `deliver` consumed (null on an empty-pool no-op);\n // read off below into the step's additive `delivered` field. `undefined` for\n // every non-deliver event kind.\n let delivered: InFlight | null = null;\n if (event.kind === \"deliver\") {\n delivered = deliverOne(world, event.index);\n } else {\n // timer / client: drive the acting node's verb directly.\n world.clock += 1;\n const at = world.clock;\n const actor = event.node;\n const node = world.handles.get(actor);\n const state = world.states[actor];\n if (node && state) {\n const inbound: RaftMsg<number> =\n event.kind === \"timer\"\n ? event.timer === \"election\"\n ? { _tag: \"election_timeout_fired\", at }\n : { _tag: \"heartbeat_fired\", at }\n : { _tag: \"client_command\", command: event.command };\n const [next, cmds] = applyInbound(node, state, inbound, at);\n world.states[actor] = next;\n park(world, actor, cmds);\n }\n }\n\n steps.push({\n event,\n pending: world.pending.length,\n states: snapshot(world.states),\n // Additive read-off: present only when a `deliver` actually consumed a\n // message. Absent (key omitted) for timer/client and empty-pool delivers,\n // so the step is structurally identical to a pre-#144 trace.\n ...(delivered ? { delivered: deliveredOf(delivered) } : {}),\n });\n}\n\n/** Deep-freeze-free structural copy of a cluster snapshot (states are already immutable values). */\nfunction snapshot(states: Record<NodeId, RaftState<number>>): ClusterStates {\n return { ...states };\n}\n\n/**\n * Run a schedule against a fresh N-node cluster, recording the full trace. Pure\n * given `(configs, schedule)`: no clock, no RNG, no IO — the only \"time\" is the\n * driver's logical clock, advanced one tick per event. Each node gets a\n * fixed-jitter rng derived from its index (distinct but deterministic timeouts).\n */\nexport function runSchedule(\n configs: readonly RaftConfig[],\n schedule: Schedule,\n): SimTrace {\n const handles = new Map<NodeId, RaftNode<number>>();\n const states: Record<NodeId, RaftState<number>> = {};\n configs.forEach((config, i) => {\n const node = createRaftNode<number>(config, fixedRng(i, configs.length));\n handles.set(config.self, node);\n states[config.self] = node.init();\n });\n\n const world: SimWorld = {\n handles,\n states,\n pending: [],\n clock: 0,\n down: new Set(),\n };\n const steps: SimStep[] = [];\n\n for (const event of schedule) {\n applyEvent(world, event, steps);\n }\n\n return {\n nodes: configs.map((c) => c.self),\n steps,\n final: snapshot(world.states),\n };\n}\n\n/**\n * Replay a schedule from a fresh cluster. Identical to {@link runSchedule} (it\n * IS `runSchedule`) — exposed under a name that documents intent at the call\n * site: a replay is meant to reproduce a prior run's trace byte-for-byte.\n */\nexport function replaySchedule(\n configs: readonly RaftConfig[],\n schedule: Schedule,\n): SimTrace {\n return runSchedule(configs, schedule);\n}\n\n// ===========================================================================\n// Comparison helpers (for the determinism property + counterexample reports)\n// ===========================================================================\n\n/**\n * Structural equality of two cluster snapshots. Used by the determinism\n * property: a run and its replay must produce identical per-node state for every\n * node. (Tests use vitest `toEqual` for the same comparison; this is the\n * dependency-free version the driver can expose for ad-hoc checks.)\n */\nexport function clusterStatesEqual(\n a: ClusterStates,\n b: ClusterStates,\n): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n// ===========================================================================\n// Re-exports the safety suite reads (so it imports the harness, not ./index)\n// ===========================================================================\n\nexport type { LogEntry, NodeId, RaftConfig, RaftState };\n"]}
@@ -7,6 +7,16 @@ declare function formOf(machine: {
7
7
  update: object;
8
8
  __form?: UpdateForm;
9
9
  }): UpdateForm;
10
+ declare function applyCell<S, M extends {
11
+ type: string;
12
+ }, C extends Cmd>(machine: {
13
+ update: object;
14
+ __form?: UpdateForm;
15
+ }, state: S, msg: M): readonly [S, readonly C[]];
16
+ declare function msgKeysOf(machine: {
17
+ update: object;
18
+ __form?: UpdateForm;
19
+ }): readonly string[];
10
20
  type Reducer<S, M extends {
11
21
  type: string;
12
22
  }, C extends Cmd> = {
@@ -175,4 +185,4 @@ declare function foldMsgs<S, M extends {
175
185
  type: string;
176
186
  }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, base: S, msgs: readonly M[]): S;
177
187
 
178
- export { Cmd as C, type Interpret as I, type Machine as M, type NoCtx as N, type Port as P, type Reducer as R, type Sub as S, type Transitions as T, type UpdateForm as U, type SubId as a, type ContextFree as b, type PortEmitter as c, type Subscribe as d, type SyncReturn as e, detectUpdateForm as f, foldMsgs as g, formOf as h, subId as s };
188
+ export { Cmd as C, type Interpret as I, type Machine as M, type NoCtx as N, type Port as P, type Reducer as R, type Sub as S, type Transitions as T, type UpdateForm as U, type SubId as a, type ContextFree as b, type PortEmitter as c, type Subscribe as d, type SyncReturn as e, applyCell as f, detectUpdateForm as g, foldMsgs as h, formOf as i, msgKeysOf as m, subId as s };
@@ -1,5 +1,5 @@
1
- import { S as Sub } from '../core-CymXZCeY.js';
2
- import { S as SubscribeHandler } from '../types-N9LZbdnr.js';
1
+ import { S as Sub } from '../core-BoB6t7dB.js';
2
+ import { S as SubscribeHandler } from '../types-B-8bcAp8.js';
3
3
 
4
4
  /**
5
5
  * @demlik/tea/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock
@@ -1,6 +1,6 @@
1
- export { deadlineExceeded, deadlineSub, subscribeDeadline } from '../chunk-QGZW4W3E.js';
1
+ export { deadlineExceeded, deadlineSub, subscribeDeadline } from '../chunk-K7Q23EQ4.js';
2
2
  import '../chunk-2YFLOCWX.js';
3
- import '../chunk-2WZHS4P2.js';
4
- import '../chunk-BNJYE4AU.js';
3
+ import '../chunk-MMH7CW5K.js';
4
+ import '../chunk-QLA76GIT.js';
5
5
  //# sourceMappingURL=index.js.map
6
6
  //# sourceMappingURL=index.js.map
@@ -9,6 +9,12 @@
9
9
  * coalesce-vs-drop policies are different enough that sharing code would hide
10
10
  * the distinction. Read one to understand the other; do not couple them.
11
11
  *
12
+ * Layering with `@demlik/tea/throttled-input`: `debounce` and `throttle` are
13
+ * the two host-boundary PRIMITIVES — two distinct algorithms (collapse-a-burst
14
+ * vs at-most-once-per-window) behind one signature shape. `throttled-input` is
15
+ * NOT a third primitive: it is a TEA machine composed OVER them that
16
+ * re-exports both (see its module doc for the full layering map).
17
+ *
12
18
  * WHAT THIS IS NOT — and why it lives outside `update`:
13
19
  *
14
20
  * - NOT a reducer / Transitions cell. Reducers are PURE (invariant 2 — same
@@ -1,3 +1,3 @@
1
- export { debounce } from '../chunk-EQRUAP2A.js';
1
+ export { debounce } from '../chunk-J6527VRQ.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -1,15 +1,15 @@
1
1
  import { Store, Runtime, BootingRuntime } from '../index.js';
2
- import { P as PendingEffectsRecorder, b as EffectOwed, a as EffectConfirmed } from '../durable-effects-2MDxDs45.js';
3
- export { D as DeliveryId, E as EffectLedgerEvent, O as OwedEffect, d as PendingEffectsLedger, c as applyEffectEvent, e as emptyLedger, f as foldLedger, i as isOwed, p as pendingEffectsLedger, s as survivingEffects } from '../durable-effects-2MDxDs45.js';
2
+ import { P as PendingEffectsRecorder, b as EffectOwed, a as EffectConfirmed } from '../durable-effects-CO9Uh971.js';
3
+ export { D as DeliveryId, E as EffectLedgerEvent, O as OwedEffect, d as PendingEffectsLedger, c as applyEffectEvent, e as emptyLedger, f as foldLedger, i as isOwed, p as pendingEffectsLedger, s as survivingEffects } from '../durable-effects-CO9Uh971.js';
4
4
  import { RetryPolicy, Rng } from '../retry-backoff/index.js';
5
- import { C as Cmd, S as Sub, M as Machine } from '../core-CymXZCeY.js';
5
+ import { C as Cmd, S as Sub, M as Machine } from '../core-BoB6t7dB.js';
6
6
  import { AgentTurn, AgentState, AgentMachineMsg, AgentEvent, AgentStatus } from '../agent/index.js';
7
7
  import '../fan-out/index.js';
8
8
  import '../work-queue/index.js';
9
9
  import '../llm-call/index.js';
10
10
  import '../resilient-call/index.js';
11
11
  import '../cache/index.js';
12
- import '../types-N9LZbdnr.js';
12
+ import '../types-B-8bcAp8.js';
13
13
  import '../circuit-breaker/index.js';
14
14
  import '../deadline/index.js';
15
15
  import '../protocol-B5YmJr0d.js';
@@ -173,7 +173,8 @@ interface StepEngine<R, I, O> {
173
173
  /**
174
174
  * Settle the posted tool result into the run, then RESUME from the durable
175
175
  * checkpoint: dispatch the settle and run to DISPATCH-QUIESCENCE (e.g.
176
- * `dispatchToIdle(runtime, settleMsg)`). Called at most once per distinct
176
+ * `await runtime.dispatch(settleMsg)` — dispatch drains the follow-up chain
177
+ * by default, #50). Called at most once per distinct
177
178
  * `callId` — `stepHost` dedupes re-POSTs via `idempotent-intake` BEFORE
178
179
  * calling this, so the side effect runs exactly once. On the first request
179
180
  * (`posted === null`) `stepHost` skips settle and calls {@link nextStep}
@@ -1187,35 +1188,25 @@ declare function sseFromAgentEvents<R, Frame>(runtime: {
1187
1188
  type: K;
1188
1189
  }>) => void): () => void;
1189
1190
  }, hub: SseHub<Frame>, toFrame: (event: AgentEvent<R>) => Frame | null): () => void;
1190
- /**
1191
- * Dispatch `msg` and resolve only once the runtime has drained every follow-up
1192
- * Msg the transition (transitively) enqueued — run-to-quiescence. Sugar over
1193
- * `await runtime.dispatch(msg); await runtime.idle();`.
1194
- *
1195
- * Replaces the consumer's / test's `until(() => cond)` poll after a `dispatch`
1196
- * for the common "dispatch, then let the loop settle" case. (A poll is still
1197
- * the right tool when waiting on an EXTERNAL event — e.g. a WS reply — that the
1198
- * runtime cannot enqueue itself.)
1199
- */
1200
- declare function dispatchToIdle<M extends {
1201
- type: string;
1202
- }>(runtime: Runtime<unknown, M>, msg: M): Promise<void>;
1203
1191
  /**
1204
1192
  * What the consumer supplies to {@link createAgentHost} — the domain mappings,
1205
1193
  * nothing of the wiring. `Stage`/`P`/`O`/`R` are the agent slice's type
1206
1194
  * parameters (the consumer names them once); `Frame` is its SSE event shape.
1195
+ * `C`/`U`/`Ctx` are the machine's own Cmd/Sub/Ctx — inferred from
1196
+ * `buildMachine` at the `createAgentHost` call site (never name them by hand;
1197
+ * an erased `any` here cost consumers all checking on this layer — #278).
1207
1198
  */
1208
- interface AgentHostConfig<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame> {
1199
+ interface AgentHostConfig<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame, C extends Cmd = Cmd, U extends Sub = Sub, Ctx = unknown> {
1209
1200
  /**
1210
1201
  * Build the wired agent machine. Called once per host build (per activation),
1211
1202
  * AFTER the previous runtime — if any — was torn down. The consumer wires its
1212
1203
  * per-tool interpret here (`agent.toMachine({ toolInterpret })`).
1213
1204
  */
1214
- readonly buildMachine: () => Machine<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, any, any, any>;
1205
+ readonly buildMachine: () => Machine<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, C, U, Ctx>;
1215
1206
  /** The durable `Store` for the agent slice (typically `doStore(storage, parse)`). */
1216
1207
  readonly store: Store<AgentState<Stage, P, O, R>>;
1217
- /** The Ctx the machine threads to its interpret cells. */
1218
- readonly ctx: unknown;
1208
+ /** The Ctx the machine threads to its interpret cells — checked against `buildMachine`'s. */
1209
+ readonly ctx: Ctx;
1219
1210
  /**
1220
1211
  * The run-terminality predicate (#46) — what makes `result()` first-class.
1221
1212
  * Defaults to the agent's own terminal phases (`done` / `failed`).
@@ -1296,7 +1287,7 @@ interface AgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R,
1296
1287
  * // start: await (await host.runtime()).dispatch({ type: "agent_start", ... });
1297
1288
  * // test: (await host.status()).kind === "suspended"
1298
1289
  */
1299
- declare function createAgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame>(config: AgentHostConfig<Stage, P, O, R, Frame>): AgentHost<Stage, P, O, R, Frame>;
1290
+ declare function createAgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame, C extends Cmd = Cmd, U extends Sub = Sub, Ctx = unknown>(config: AgentHostConfig<Stage, P, O, R, Frame, C, U, Ctx>): AgentHost<Stage, P, O, R, Frame>;
1300
1291
 
1301
1292
  /**
1302
1293
  * @demlik/tea/do — the idempotent-effect brick (the receiver/effect-side dedup).
@@ -1772,4 +1763,4 @@ interface DoStoreOptions<S> {
1772
1763
  */
1773
1764
  declare function doStore<S>(storage: DurableObjectStorage, parse: (raw: unknown) => S | null, keyOrOptions?: string | DoStoreOptions<S>): Store<S>;
1774
1765
 
1775
- export { type AgentHost, type AgentHostConfig, type AlarmStorage, type AppliedEffects, type AppliedEffectsEvent, type AppliedEffectsGuard, type AttachableSocket, type BroadcastOptions, type BroadcastReport, type DeferredGateway, type DoStoreOptions, type DurableCommandCarrier, type DurableTimer, type DurableTimerConfig, type EffectApplied, EffectConfirmed, type EffectForgotten, type EffectKey, EffectOwed, type EventLogRange, type EventSourcedOptions, type EventSourcedStore, type ExecuteStep, type HibernatableCtx, type IdempotentEffect, type IdempotentOutcome, type NextStep, PendingEffectsRecorder, type PersistedEvent, type PresenceCtx, type PresenceSocket, type PresenceUpgrade, type Projection, type ProjectionId, type ProjectionRegistry, type ProjectionRunner, type ProjectionUpdate, type RegisterOptions, type ResumePort, type RunStepLoopConfig, type SseHub, type StepCtx, type StepEngine, type StepHostConfig, type StepLoopOutcome, type StepOutcome, type StepRequest, type StepResponse, type StepResult, type StepTransport, WS_READY_STATE_OPEN, acceptCommandSocket, acceptDurableCommandSocket, acceptPresenceSocket, agentIsResumable, appliedEffects, applyAppliedEvent, autoBoot, bootResume, broadcast, broadcastFrame, broadcastHibernatable, constantTimeEqual, createAgentHost, deferredGateway, dispatchToIdle, doEventSourcedStore, doStore, driveProjections, durableCommandCarrier, durableDeferredGateway, durableTimer, emptyApplied, foldApplied, idempotentEffect, isApplied, mintRunToken, presenceCount, projectionIdString, projectionRegistry, rebuildProjection, registerHibernatableSocket, reissueSurvivingEffects, runProjection, runStepLoop, sseFromAgentEvents, sseHub, sseProjection, stepHost };
1766
+ export { type AgentHost, type AgentHostConfig, type AlarmStorage, type AppliedEffects, type AppliedEffectsEvent, type AppliedEffectsGuard, type AttachableSocket, type BroadcastOptions, type BroadcastReport, type DeferredGateway, type DoStoreOptions, type DurableCommandCarrier, type DurableTimer, type DurableTimerConfig, type EffectApplied, EffectConfirmed, type EffectForgotten, type EffectKey, EffectOwed, type EventLogRange, type EventSourcedOptions, type EventSourcedStore, type ExecuteStep, type HibernatableCtx, type IdempotentEffect, type IdempotentOutcome, type NextStep, PendingEffectsRecorder, type PersistedEvent, type PresenceCtx, type PresenceSocket, type PresenceUpgrade, type Projection, type ProjectionId, type ProjectionRegistry, type ProjectionRunner, type ProjectionUpdate, type RegisterOptions, type ResumePort, type RunStepLoopConfig, type SseHub, type StepCtx, type StepEngine, type StepHostConfig, type StepLoopOutcome, type StepOutcome, type StepRequest, type StepResponse, type StepResult, type StepTransport, WS_READY_STATE_OPEN, acceptCommandSocket, acceptDurableCommandSocket, acceptPresenceSocket, agentIsResumable, appliedEffects, applyAppliedEvent, autoBoot, bootResume, broadcast, broadcastFrame, broadcastHibernatable, constantTimeEqual, createAgentHost, deferredGateway, doEventSourcedStore, doStore, driveProjections, durableCommandCarrier, durableDeferredGateway, durableTimer, emptyApplied, foldApplied, idempotentEffect, isApplied, mintRunToken, presenceCount, projectionIdString, projectionRegistry, rebuildProjection, registerHibernatableSocket, reissueSurvivingEffects, runProjection, runStepLoop, sseFromAgentEvents, sseHub, sseProjection, stepHost };
package/dist/do/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { status, agentBootMsg, agentEvents } from '../chunk-TC4HWABA.js';
2
- import '../chunk-TAO4YOLO.js';
1
+ import { status, agentBootMsg, agentEvents } from '../chunk-S3FQN4SL.js';
2
+ import '../chunk-7W4LMPUW.js';
3
+ import '../chunk-PS23H6KQ.js';
3
4
  export { applyEffectEvent, emptyLedger, foldLedger, isOwed, pendingEffectsLedger, survivingEffects } from '../chunk-3545BOYL.js';
4
- import '../chunk-D5EQDLDV.js';
5
- import '../chunk-EIZJTYKR.js';
6
- import '../chunk-RVJFGRGK.js';
5
+ import '../chunk-EHIX7NYZ.js';
6
+ import '../chunk-27N2SPTN.js';
7
7
  import '../chunk-JELSZN25.js';
8
8
  import '../chunk-UV6UWGEO.js';
9
9
  import { createIntake } from '../chunk-LEC4U6CX.js';
10
- import '../chunk-QGZW4W3E.js';
10
+ import '../chunk-K7Q23EQ4.js';
11
11
  import { defaultRetryPolicy, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-5VNWZADP.js';
12
12
  import '../chunk-2YOE5PIS.js';
13
13
  import '../chunk-BYQUGDD3.js';
@@ -18,8 +18,8 @@ import '../chunk-HJO6VNQI.js';
18
18
  import '../chunk-TGG26XMK.js';
19
19
  import '../chunk-NSDFA6M6.js';
20
20
  import '../chunk-2YFLOCWX.js';
21
- import { run, replay } from '../chunk-2WZHS4P2.js';
22
- import '../chunk-BNJYE4AU.js';
21
+ import { run, replay } from '../chunk-MMH7CW5K.js';
22
+ import '../chunk-QLA76GIT.js';
23
23
 
24
24
  // src/do/durable-timer.ts
25
25
  function durableTimer(config) {
@@ -361,10 +361,6 @@ function sseFromAgentEvents(runtime, hub, toFrame) {
361
361
  for (const off of offs) off();
362
362
  };
363
363
  }
364
- async function dispatchToIdle(runtime, msg) {
365
- await runtime.dispatch(msg);
366
- await runtime.idle();
367
- }
368
364
  function createAgentHost(config) {
369
365
  const sse = sseHub();
370
366
  const now = config.now ?? Date.now;
@@ -695,6 +691,6 @@ function doStore(storage, parse, keyOrOptions = DEFAULT_STATE_KEY) {
695
691
  };
696
692
  }
697
693
 
698
- export { WS_READY_STATE_OPEN, acceptCommandSocket, acceptDurableCommandSocket, acceptPresenceSocket, agentIsResumable, appliedEffects, applyAppliedEvent, autoBoot, bootResume, broadcast, broadcastFrame, broadcastHibernatable, constantTimeEqual, createAgentHost, deferredGateway, dispatchToIdle, doEventSourcedStore, doStore, driveProjections, durableCommandCarrier, durableDeferredGateway, durableTimer, emptyApplied, foldApplied, idempotentEffect, isApplied, mintRunToken, presenceCount, projectionIdString, projectionRegistry, rebuildProjection, registerHibernatableSocket, reissueSurvivingEffects, runProjection, runStepLoop, sseFromAgentEvents, sseHub, sseProjection, stepHost };
694
+ export { WS_READY_STATE_OPEN, acceptCommandSocket, acceptDurableCommandSocket, acceptPresenceSocket, agentIsResumable, appliedEffects, applyAppliedEvent, autoBoot, bootResume, broadcast, broadcastFrame, broadcastHibernatable, constantTimeEqual, createAgentHost, deferredGateway, doEventSourcedStore, doStore, driveProjections, durableCommandCarrier, durableDeferredGateway, durableTimer, emptyApplied, foldApplied, idempotentEffect, isApplied, mintRunToken, presenceCount, projectionIdString, projectionRegistry, rebuildProjection, registerHibernatableSocket, reissueSurvivingEffects, runProjection, runStepLoop, sseFromAgentEvents, sseHub, sseProjection, stepHost };
699
695
  //# sourceMappingURL=index.js.map
700
696
  //# sourceMappingURL=index.js.map