@demlik/tea 0.1.0 → 0.1.1

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 (238) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/index.d.ts +907 -0
  3. package/dist/agent/index.js +20 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/authed-call/index.d.ts +119 -0
  6. package/dist/authed-call/index.js +148 -0
  7. package/dist/authed-call/index.js.map +1 -0
  8. package/dist/await-terminal/index.d.ts +99 -0
  9. package/dist/await-terminal/index.js +75 -0
  10. package/dist/await-terminal/index.js.map +1 -0
  11. package/dist/batch-window/index.d.ts +259 -0
  12. package/dist/batch-window/index.js +43 -0
  13. package/dist/batch-window/index.js.map +1 -0
  14. package/dist/cache/index.d.ts +2 -2
  15. package/dist/cache/index.js +2 -50
  16. package/dist/cache/index.js.map +1 -1
  17. package/dist/{chunk-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
  18. package/dist/chunk-2WZHS4P2.js.map +1 -0
  19. package/dist/chunk-2YOE5PIS.js +45 -0
  20. package/dist/chunk-2YOE5PIS.js.map +1 -0
  21. package/dist/chunk-3545BOYL.js +65 -0
  22. package/dist/chunk-3545BOYL.js.map +1 -0
  23. package/dist/chunk-5VNWZADP.js +49 -0
  24. package/dist/chunk-5VNWZADP.js.map +1 -0
  25. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  26. package/dist/chunk-7FYQGDMF.js.map +1 -0
  27. package/dist/chunk-7VEG4KHB.js +202 -0
  28. package/dist/chunk-7VEG4KHB.js.map +1 -0
  29. package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
  30. package/dist/chunk-A56GNPI7.js.map +1 -0
  31. package/dist/chunk-BNJYE4AU.js +157 -0
  32. package/dist/chunk-BNJYE4AU.js.map +1 -0
  33. package/dist/chunk-BYQUGDD3.js +16 -0
  34. package/dist/chunk-BYQUGDD3.js.map +1 -0
  35. package/dist/chunk-D5EQDLDV.js +186 -0
  36. package/dist/chunk-D5EQDLDV.js.map +1 -0
  37. package/dist/chunk-EDGDR5XX.js +60 -0
  38. package/dist/chunk-EDGDR5XX.js.map +1 -0
  39. package/dist/chunk-EIZJTYKR.js +93 -0
  40. package/dist/chunk-EIZJTYKR.js.map +1 -0
  41. package/dist/chunk-EQRUAP2A.js +56 -0
  42. package/dist/chunk-EQRUAP2A.js.map +1 -0
  43. package/dist/chunk-FH5RUIIL.js +61 -0
  44. package/dist/chunk-FH5RUIIL.js.map +1 -0
  45. package/dist/chunk-HJO6VNQI.js +15 -0
  46. package/dist/chunk-HJO6VNQI.js.map +1 -0
  47. package/dist/chunk-IFBQJYGI.js +132 -0
  48. package/dist/chunk-IFBQJYGI.js.map +1 -0
  49. package/dist/chunk-IN33Q4VJ.js +52 -0
  50. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  51. package/dist/chunk-JELSZN25.js +47 -0
  52. package/dist/chunk-JELSZN25.js.map +1 -0
  53. package/dist/chunk-K575PT4S.js +58 -0
  54. package/dist/chunk-K575PT4S.js.map +1 -0
  55. package/dist/chunk-LEC4U6CX.js +91 -0
  56. package/dist/chunk-LEC4U6CX.js.map +1 -0
  57. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  58. package/dist/chunk-NSDFA6M6.js.map +1 -0
  59. package/dist/chunk-QGZW4W3E.js +20 -0
  60. package/dist/chunk-QGZW4W3E.js.map +1 -0
  61. package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
  62. package/dist/chunk-RGTPPF4U.js.map +1 -0
  63. package/dist/chunk-RVJFGRGK.js +222 -0
  64. package/dist/chunk-RVJFGRGK.js.map +1 -0
  65. package/dist/chunk-RVU3RBE6.js +31 -0
  66. package/dist/chunk-RVU3RBE6.js.map +1 -0
  67. package/dist/chunk-SC3FAFZN.js +53 -0
  68. package/dist/chunk-SC3FAFZN.js.map +1 -0
  69. package/dist/chunk-SINXE47Y.js +55 -0
  70. package/dist/chunk-SINXE47Y.js.map +1 -0
  71. package/dist/chunk-TAO4YOLO.js +87 -0
  72. package/dist/chunk-TAO4YOLO.js.map +1 -0
  73. package/dist/chunk-TC4HWABA.js +506 -0
  74. package/dist/chunk-TC4HWABA.js.map +1 -0
  75. package/dist/chunk-TGG26XMK.js +52 -0
  76. package/dist/chunk-TGG26XMK.js.map +1 -0
  77. package/dist/chunk-UV6UWGEO.js +147 -0
  78. package/dist/chunk-UV6UWGEO.js.map +1 -0
  79. package/dist/chunk-UYWUVEP6.js +158 -0
  80. package/dist/chunk-UYWUVEP6.js.map +1 -0
  81. package/dist/chunk-WUV3PSCZ.js +54 -0
  82. package/dist/chunk-WUV3PSCZ.js.map +1 -0
  83. package/dist/chunk-XRXKH7KA.js +268 -0
  84. package/dist/chunk-XRXKH7KA.js.map +1 -0
  85. package/dist/circuit-breaker/index.js +1 -51
  86. package/dist/circuit-breaker/index.js.map +1 -1
  87. package/dist/core-CymXZCeY.d.ts +178 -0
  88. package/dist/deadline/index.d.ts +24 -5
  89. package/dist/deadline/index.js +4 -18
  90. package/dist/deadline/index.js.map +1 -1
  91. package/dist/debounce/index.js +1 -54
  92. package/dist/debounce/index.js.map +1 -1
  93. package/dist/devtools/index.d.ts +1 -1
  94. package/dist/devtools/index.js +18 -6
  95. package/dist/devtools/index.js.map +1 -1
  96. package/dist/do/index.d.ts +1732 -92
  97. package/dist/do/index.js +677 -31
  98. package/dist/do/index.js.map +1 -1
  99. package/dist/durable-effects-2MDxDs45.d.ts +180 -0
  100. package/dist/extension/index.d.ts +31 -13
  101. package/dist/extension/index.js +5 -3
  102. package/dist/extension/index.js.map +1 -1
  103. package/dist/extension/react.js +8 -3
  104. package/dist/extension/react.js.map +1 -1
  105. package/dist/extension/subs/index.d.ts +2 -2
  106. package/dist/extension/subs/index.js.map +1 -1
  107. package/dist/extension/test-utils.js +11 -3
  108. package/dist/extension/test-utils.js.map +1 -1
  109. package/dist/fan-out/index.d.ts +239 -0
  110. package/dist/fan-out/index.js +5 -0
  111. package/dist/fan-out/index.js.map +1 -0
  112. package/dist/idempotency/adapter.d.ts +62 -0
  113. package/dist/idempotency/adapter.js +4 -0
  114. package/dist/idempotency/adapter.js.map +1 -0
  115. package/dist/idempotency/index.js +1 -56
  116. package/dist/idempotency/index.js.map +1 -1
  117. package/dist/idempotent-intake/index.d.ts +186 -0
  118. package/dist/idempotent-intake/index.js +7 -0
  119. package/dist/idempotent-intake/index.js.map +1 -0
  120. package/dist/index.d.ts +341 -191
  121. package/dist/index.js +2 -1
  122. package/dist/llm-call/index.d.ts +309 -0
  123. package/dist/llm-call/index.js +14 -0
  124. package/dist/llm-call/index.js.map +1 -0
  125. package/dist/machine-viz/index.d.ts +1 -1
  126. package/dist/machine-viz/index.js +1 -1
  127. package/dist/machine-viz/index.js.map +1 -1
  128. package/dist/mem/index.d.ts +1 -0
  129. package/dist/monitored-run/index.d.ts +253 -0
  130. package/dist/monitored-run/index.js +10 -0
  131. package/dist/monitored-run/index.js.map +1 -0
  132. package/dist/node/index.d.ts +5 -5
  133. package/dist/node/index.js.map +1 -1
  134. package/dist/paginated-walk/index.d.ts +234 -0
  135. package/dist/paginated-walk/index.js +15 -0
  136. package/dist/paginated-walk/index.js.map +1 -0
  137. package/dist/paginator/index.d.ts +271 -0
  138. package/dist/paginator/index.js +3 -0
  139. package/dist/paginator/index.js.map +1 -0
  140. package/dist/parity/index.d.ts +86 -0
  141. package/dist/parity/index.js +77 -0
  142. package/dist/parity/index.js.map +1 -0
  143. package/dist/pbt/arbitraries/index.d.ts +1 -1
  144. package/dist/pbt/arbitraries/index.js +1 -1
  145. package/dist/pbt/index.d.ts +1 -1
  146. package/dist/pbt/index.js +5 -4
  147. package/dist/pbt/index.js.map +1 -1
  148. package/dist/pbt/runners/index.d.ts +1 -1
  149. package/dist/pbt/runners/index.js +3 -2
  150. package/dist/poller/index.d.ts +288 -0
  151. package/dist/poller/index.js +79 -0
  152. package/dist/poller/index.js.map +1 -0
  153. package/dist/prediction/index.d.ts +109 -0
  154. package/dist/prediction/index.js +4 -0
  155. package/dist/prediction/index.js.map +1 -0
  156. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  157. package/dist/pure/index.d.ts +2 -0
  158. package/dist/pure/index.js +4 -0
  159. package/dist/pure/index.js.map +1 -0
  160. package/dist/raft/demo.d.ts +105 -0
  161. package/dist/raft/demo.js +172 -0
  162. package/dist/raft/demo.js.map +1 -0
  163. package/dist/raft/index.d.ts +548 -0
  164. package/dist/raft/index.js +7 -0
  165. package/dist/raft/index.js.map +1 -0
  166. package/dist/raft/sim.d.ts +222 -0
  167. package/dist/raft/sim.js +8 -0
  168. package/dist/raft/sim.js.map +1 -0
  169. package/dist/raft/timeline.d.ts +46 -0
  170. package/dist/raft/timeline.js +62 -0
  171. package/dist/raft/timeline.js.map +1 -0
  172. package/dist/rate-limit/index.js +1 -43
  173. package/dist/rate-limit/index.js.map +1 -1
  174. package/dist/react/index.d.ts +2 -1
  175. package/dist/react/index.js +39 -20
  176. package/dist/react/index.js.map +1 -1
  177. package/dist/reconciler/index.d.ts +192 -0
  178. package/dist/reconciler/index.js +166 -0
  179. package/dist/reconciler/index.js.map +1 -0
  180. package/dist/recorder/index.d.ts +7 -3
  181. package/dist/recorder/index.js +1 -155
  182. package/dist/recorder/index.js.map +1 -1
  183. package/dist/resilient-call/index.d.ts +251 -0
  184. package/dist/resilient-call/index.js +13 -0
  185. package/dist/resilient-call/index.js.map +1 -0
  186. package/dist/retry-backoff/index.d.ts +42 -5
  187. package/dist/retry-backoff/index.js +1 -35
  188. package/dist/retry-backoff/index.js.map +1 -1
  189. package/dist/saga/index.d.ts +225 -0
  190. package/dist/saga/index.js +150 -0
  191. package/dist/saga/index.js.map +1 -0
  192. package/dist/snapshot/index.d.ts +334 -0
  193. package/dist/snapshot/index.js +5 -0
  194. package/dist/snapshot/index.js.map +1 -0
  195. package/dist/subs/index.d.ts +48 -10
  196. package/dist/subs/index.js +1 -1
  197. package/dist/testing/index.d.ts +107 -2
  198. package/dist/testing/index.js +204 -6
  199. package/dist/testing/index.js.map +1 -1
  200. package/dist/throttle/index.js +1 -52
  201. package/dist/throttle/index.js.map +1 -1
  202. package/dist/throttled-input/index.d.ts +372 -0
  203. package/dist/throttled-input/index.js +88 -0
  204. package/dist/throttled-input/index.js.map +1 -0
  205. package/dist/token-refresh/index.d.ts +215 -0
  206. package/dist/token-refresh/index.js +6 -0
  207. package/dist/token-refresh/index.js.map +1 -0
  208. package/dist/trace-replay/index.d.ts +11 -2
  209. package/dist/trace-replay/index.js +3 -55
  210. package/dist/trace-replay/index.js.map +1 -1
  211. package/dist/{types-CwBC5wMn.d.ts → types-N9LZbdnr.d.ts} +1 -1
  212. package/dist/with-deadline/index.d.ts +196 -0
  213. package/dist/with-deadline/index.js +130 -0
  214. package/dist/with-deadline/index.js.map +1 -0
  215. package/dist/with-resilience/index.d.ts +205 -0
  216. package/dist/with-resilience/index.js +274 -0
  217. package/dist/with-resilience/index.js.map +1 -0
  218. package/dist/with-telemetry/index.d.ts +149 -0
  219. package/dist/with-telemetry/index.js +95 -0
  220. package/dist/with-telemetry/index.js.map +1 -0
  221. package/dist/work-queue/adapter.d.ts +82 -0
  222. package/dist/work-queue/adapter.js +4 -0
  223. package/dist/work-queue/adapter.js.map +1 -0
  224. package/dist/work-queue/index.d.ts +25 -11
  225. package/dist/work-queue/index.js +1 -46
  226. package/dist/work-queue/index.js.map +1 -1
  227. package/dist/work-queue/ops.d.ts +69 -0
  228. package/dist/work-queue/ops.js +3 -0
  229. package/dist/work-queue/ops.js.map +1 -0
  230. package/dist/workflow/index.d.ts +444 -0
  231. package/dist/workflow/index.js +225 -0
  232. package/dist/workflow/index.js.map +1 -0
  233. package/package.json +162 -25
  234. package/dist/chunk-CMG2KMWX.js.map +0 -1
  235. package/dist/chunk-PYBQREFU.js.map +0 -1
  236. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  237. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  238. package/dist/chunk-ZRUST6BK.js.map +0 -1
@@ -1,9 +1,1685 @@
1
- import { Store, SubId, Sub } from '../index.js';
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';
4
+ import { RetryPolicy, Rng } from '../retry-backoff/index.js';
5
+ import { C as Cmd, S as Sub, M as Machine } from '../core-CymXZCeY.js';
6
+ import { AgentTurn, AgentState, AgentMachineMsg, AgentEvent, AgentStatus } from '../agent/index.js';
7
+ import '../fan-out/index.js';
8
+ import '../work-queue/index.js';
9
+ import '../llm-call/index.js';
10
+ import '../resilient-call/index.js';
11
+ import '../cache/index.js';
12
+ import '../types-N9LZbdnr.js';
13
+ import '../circuit-breaker/index.js';
14
+ import '../deadline/index.js';
15
+ import '../protocol-B5YmJr0d.js';
16
+ import '../rate-limit/index.js';
17
+ import '../monitored-run/index.js';
18
+ import '../snapshot/index.js';
19
+
20
+ /**
21
+ * @demlik/tea/do — `stepHost`, the HTTP-PULL control carrier (#92).
22
+ *
23
+ * ## Carrier selection — pick by WHO INITIATES the message
24
+ *
25
+ * `stepHost` is a SIBLING to {@link acceptCommandSocket}, never a replacement.
26
+ * Both carriers commonly run together in ONE Durable Object; choose between
27
+ * them per *plane*, by who sends the next message:
28
+ *
29
+ * - **Pull (this file — `stepHost` over `/step`)** — the CONTROL plane. One
30
+ * driver (the hands) initiates every message: it asks for the next tool,
31
+ * runs it, posts the result, asks again. There is exactly one outstanding
32
+ * question at a time and the DO is COLD between steps — nothing is owed
33
+ * while no request is in flight. Hibernation is the resting state, not a
34
+ * failure: each POST wakes the DO, advances the run one step from its
35
+ * durable checkpoint, and lets it hibernate again.
36
+ * - **Push (`acceptCommandSocket` / `sseHub` — WS + SSE)** — the VIEW /
37
+ * fan-out plane. The DO initiates UNSOLICITED messages: broadcast a frame
38
+ * to every connected client, stream a live dashboard, push an event no
39
+ * client asked for. A long-lived socket is the right carrier because the
40
+ * sender is the DO, not a polling client.
41
+ *
42
+ * The rule in one line: **pull when the client drives a cold-between-steps
43
+ * control loop; push when the DO drives unsolicited fan-out.** A hibernating
44
+ * agent whose only traffic is "run the next tool" wants pull — that is the case
45
+ * Binclusive #1812 / #1814 adopted this carrier for.
46
+ *
47
+ * ## Why pull is hibernation-correct
48
+ *
49
+ * The WS push carrier keeps a socket open across steps; if the DO evicts, the
50
+ * socket dies (or, with #91's `acceptDurableCommandSocket`, survives but still
51
+ * holds an in-flight round-trip the heap forgot). The pull carrier holds NO
52
+ * cross-step state in the heap. Every `/step` POST is a FRESH request that:
53
+ *
54
+ * 1. **validates** the per-run capability token (constant-time compare),
55
+ * 2. **re-arms** the eviction-safe give-up alarm via `ctx.storage.setAlarm`
56
+ * (the DO-native alarm API — the `do_alarm` Sub was dropped, see the
57
+ * `host.ts` header; the alarm is a host concern, not a machine Sub),
58
+ * 3. **settles** the posted tool result IDEMPOTENTLY through
59
+ * `idempotent-intake` — a re-POSTed result is absorbed exactly once and
60
+ * the duplicate replays the same next step,
61
+ * 4. **resumes** the run from its durable checkpoint (the #68 event-sourced
62
+ * store / #91 in-flight ledger) by dispatching the settle and running to
63
+ * DISPATCH-QUIESCENCE,
64
+ * 5. **returns** the next step (the next tool call) — or the terminal output
65
+ * read off `runtime.result()` — and lets the DO HIBERNATE.
66
+ *
67
+ * No warm loop is held between responses. The durable checkpoint IS the loop's
68
+ * continuation; the heap is disposable.
69
+ *
70
+ * ## It is ASSEMBLY over bricks tea already ships
71
+ *
72
+ * - `idempotent-intake` (`createIntake`) — the dedupe-by-key receive-once
73
+ * settle. The posted result's `callId` is the idempotency key, so a
74
+ * re-POST lands as a duplicate and replays the cached decision.
75
+ * - `poller` primitives (`retry-backoff`) — the hands-side {@link runStepLoop}
76
+ * pull loop: absolute deadline + backoff + dedupe-by-callId.
77
+ * - the #68/#91 durable checkpoint — read by the consumer's `resume` callback
78
+ * (it dispatches the settle into the runtime, whose `doEventSourcedStore`
79
+ * rebuilt state from the log on this cold wake).
80
+ *
81
+ * This file owns NONE of those primitives — it wires them into the `/step`
82
+ * request/response contract and the token lifecycle.
83
+ */
84
+
85
+ /**
86
+ * The tool result the hands POST back for the step they just executed. `callId`
87
+ * matches the step the previous response handed out (it is BOTH the correlation
88
+ * id and the idempotency key — a re-POST carries the same `callId` and is
89
+ * absorbed once). `result` is the opaque tool output `R`.
90
+ */
91
+ interface StepResult<R> {
92
+ readonly callId: string;
93
+ readonly result: R;
94
+ }
95
+ /**
96
+ * A `/step` request. `token` is the per-run capability (validated
97
+ * constant-time). `runId` names the run. `posted` is the result of the
98
+ * previously-returned step, or `null` on the FIRST request (no step has been
99
+ * handed out yet, so there is nothing to settle).
100
+ */
101
+ interface StepRequest<R> {
102
+ readonly token: string;
103
+ readonly runId: string;
104
+ readonly posted: StepResult<R> | null;
105
+ }
106
+ /**
107
+ * The next tool call to execute — the "continue" arm of a `/step` response.
108
+ * `callId` is the correlation/idempotency id the hands echo back in the next
109
+ * request's `posted`. `tool` + `input` describe the call (opaque to the host).
110
+ */
111
+ interface NextStep<I> {
112
+ readonly callId: string;
113
+ readonly tool: string;
114
+ readonly input: I;
115
+ }
116
+ /**
117
+ * A `/step` response — a discriminated union on `done`:
118
+ * - `{ done: false, step }` — execute `step`, POST its result, ask again.
119
+ * - `{ done: true, output }` — the run is terminal; `output` is its result.
120
+ */
121
+ type StepResponse<I, O> = {
122
+ readonly done: false;
123
+ readonly step: NextStep<I>;
124
+ } | {
125
+ readonly done: true;
126
+ readonly output: O;
127
+ };
128
+ /**
129
+ * Mint a fresh per-run capability token: 32 random bytes, hex-encoded. Uses the
130
+ * Web Crypto `crypto.getRandomValues` available in the Workers runtime (and in
131
+ * Node's test runtime via the global `crypto`). Persist the returned string in
132
+ * DO storage keyed by runId; hand the SAME string to the hands once, at run
133
+ * start.
134
+ */
135
+ declare function mintRunToken(randomBytes?: (n: number) => Uint8Array): string;
136
+ /**
137
+ * Constant-time string equality. Returns `true` iff `a` and `b` are
138
+ * byte-for-byte equal, in time that depends ONLY on `a.length` — never on WHERE
139
+ * the first mismatch is. A length mismatch still returns `false`, but the
140
+ * comparison loop always runs `a.length` iterations against a same-length view,
141
+ * so the early-exit timing oracle a naive `===` leaks is closed.
142
+ *
143
+ * The XOR-accumulate idiom: fold every position's difference into one
144
+ * accumulator and test it once at the end. No `break`, no short-circuit.
145
+ */
146
+ declare function constantTimeEqual(a: string, b: string): boolean;
147
+ /**
148
+ * The DO-native alarm slice `stepHost` re-arms. A real `DurableObjectStorage`
149
+ * satisfies this structurally; a test supplies a fake (no live Workers runtime).
150
+ * Only `setAlarm` is needed — the give-up deadline is re-armed on every step so
151
+ * a run abandoned mid-loop (the hands stop POSTing) is eventually evicted by the
152
+ * alarm firing, not left resident forever.
153
+ */
154
+ interface AlarmStorage {
155
+ setAlarm(scheduledTime: number): void | Promise<void>;
156
+ }
157
+ /** The minimal `ctx` slice `stepHost` reads — just the alarm-bearing storage. */
158
+ interface StepCtx {
159
+ readonly storage: AlarmStorage;
160
+ }
161
+ /**
162
+ * The run-specific operations `stepHost` orchestrates. The consumer implements
163
+ * these against its runtime + storage; `stepHost` never reaches into the agent
164
+ * machine itself (that lives in `../agent`, off-limits to this carrier).
165
+ */
166
+ interface StepEngine<R, I, O> {
167
+ /**
168
+ * The persisted capability token for `runId`, or `null` if the run is
169
+ * unknown. Read from DO storage. `stepHost` compares the request token
170
+ * against this constant-time.
171
+ */
172
+ tokenFor(runId: string): Promise<string | null> | string | null;
173
+ /**
174
+ * Settle the posted tool result into the run, then RESUME from the durable
175
+ * checkpoint: dispatch the settle and run to DISPATCH-QUIESCENCE (e.g.
176
+ * `dispatchToIdle(runtime, settleMsg)`). Called at most once per distinct
177
+ * `callId` — `stepHost` dedupes re-POSTs via `idempotent-intake` BEFORE
178
+ * calling this, so the side effect runs exactly once. On the first request
179
+ * (`posted === null`) `stepHost` skips settle and calls {@link nextStep}
180
+ * directly to hand out the first step.
181
+ */
182
+ resume(runId: string, posted: StepResult<R>): Promise<void> | void;
183
+ /**
184
+ * Read the next step the run is now waiting on — the next tool call — off the
185
+ * resumed runtime's durable state, or `null` if the run has reached its
186
+ * terminal State (then {@link terminalOutput} supplies the output).
187
+ */
188
+ nextStep(runId: string): Promise<NextStep<I> | null> | NextStep<I> | null;
189
+ /**
190
+ * Read the run's terminal output (off `runtime.result()`). Only called when
191
+ * {@link nextStep} returned `null`.
192
+ */
193
+ terminalOutput(runId: string): Promise<O> | O;
194
+ }
195
+ /** Tuning for `stepHost`'s give-up alarm. */
196
+ interface StepHostConfig {
197
+ /**
198
+ * How far ahead, in ms, to re-arm the give-up alarm on every step. A run that
199
+ * stops POSTing for longer than this is abandoned and the alarm fires (the
200
+ * consumer's `alarm()` handler tears the run down). Defaults to 5 minutes.
201
+ */
202
+ readonly giveUpAfterMs?: number;
203
+ }
204
+ /**
205
+ * A structured `/step` outcome `stepHost` returns — the response body plus the
206
+ * HTTP status the consumer's route should send. Kept transport-agnostic (no
207
+ * `Response`) so the same handler is unit-testable without a live `fetch`.
208
+ */
209
+ type StepOutcome<I, O> = {
210
+ readonly status: 200;
211
+ readonly body: StepResponse<I, O>;
212
+ } | {
213
+ readonly status: 401 | 404;
214
+ readonly body: {
215
+ readonly error: string;
216
+ };
217
+ };
218
+ /**
219
+ * Build the `/step` handler for a DO that drives one or more runs over the pull
220
+ * carrier. Returns `handle(request, now)` — call it from the DO's `/step`
221
+ * route, map the `StepOutcome` to a `Response`, and let the DO hibernate.
222
+ *
223
+ * The handler is STATELESS between calls except for its `idempotent-intake`
224
+ * dedupe ledger, which the consumer should itself persist alongside the run's
225
+ * checkpoint if cross-eviction dedupe of an identical re-POST is required; the
226
+ * in-isolate ledger here dedupes re-POSTs within one activation (the common
227
+ * "the hands' POST timed out and got retried" case), and the underlying
228
+ * `resume` is itself idempotent against the durable checkpoint, so a re-POST
229
+ * that crosses an eviction re-settles harmlessly.
230
+ *
231
+ * @param ctx the DO state slice (alarm-bearing storage)
232
+ * @param engine the run-specific operations
233
+ * @param config alarm tuning
234
+ */
235
+ declare function stepHost<R, I, O>(ctx: StepCtx, engine: StepEngine<R, I, O>, config?: StepHostConfig): {
236
+ handle(request: StepRequest<R>, now: number): Promise<StepOutcome<I, O>>;
237
+ };
238
+ /** The `/step` transport the hands POST through (injected; faked in tests). */
239
+ type StepTransport<R, I, O> = (request: StepRequest<R>) => Promise<StepResponse<I, O>>;
240
+ /** Execute one tool call and produce its result (the consumer's hands). */
241
+ type ExecuteStep<I, R> = (step: NextStep<I>) => Promise<R> | R;
242
+ /** Tuning for {@link runStepLoop}. */
243
+ interface RunStepLoopConfig {
244
+ /**
245
+ * Absolute wall-clock deadline (ms epoch). The loop stops asking once `now()`
246
+ * reaches it, surfacing a `deadline_exceeded` outcome. Mirrors the poller's
247
+ * absolute-deadline contract (a resumed loop honors the SAME moment).
248
+ */
249
+ readonly deadlineMs: number;
250
+ /** Backoff policy for FAILED `/step` POSTs. Defaults to `defaultRetryPolicy`. */
251
+ readonly retry?: RetryPolicy;
252
+ /** Clock, injected for determinism. Defaults to `Date.now`. */
253
+ readonly now?: () => number;
254
+ /** `(ms) => Promise` sleep, injected for determinism. Defaults to real sleep. */
255
+ readonly sleep?: (ms: number) => Promise<void>;
256
+ /** Backoff jitter RNG, injected for determinism. Defaults to `Math.random`. */
257
+ readonly rng?: Rng;
258
+ }
259
+ /** The terminal outcome of a full {@link runStepLoop} drive. */
260
+ type StepLoopOutcome<O> = {
261
+ readonly kind: "done";
262
+ readonly output: O;
263
+ } | {
264
+ readonly kind: "deadline_exceeded";
265
+ } | {
266
+ readonly kind: "gave_up";
267
+ readonly error: unknown;
268
+ };
269
+ /**
270
+ * Drive the pull loop to completion. Starting from `{ posted: null }`, POST
271
+ * `/step`, and for each non-terminal response execute the step and POST its
272
+ * result, until the response is terminal (`done: true`) — then return its
273
+ * output.
274
+ *
275
+ * Resilience composed from `poller`'s primitives:
276
+ * - **absolute deadline** — `config.deadlineMs`. Before each POST the loop
277
+ * checks `now() >= deadlineMs` and stops with `deadline_exceeded` rather
278
+ * than asking forever.
279
+ * - **backoff** — a FAILED POST (transport reject) is retried on the
280
+ * `retry-backoff` curve; once `shouldRetry` is exhausted the loop stops
281
+ * with `gave_up`. A success resets the retry counter.
282
+ * - **dedupe** — a step whose `callId` was already executed in THIS drive is
283
+ * not re-executed; its prior result is re-POSTed. Closes the "the host
284
+ * re-handed the same step after a duplicate settle" path (the host side is
285
+ * idempotent; the hands match it).
286
+ */
287
+ declare function runStepLoop<R, I, O>(token: string, runId: string, transport: StepTransport<R, I, O>, execute: ExecuteStep<I, R>, config: RunStepLoopConfig): Promise<StepLoopOutcome<O>>;
288
+
289
+ /**
290
+ * `@demlik/tea/do` — the durable-timer / alarm-carrier port (#180).
291
+ *
292
+ * Every native-DO grain that wants a wall-clock tick hand-rolls the SAME
293
+ * eviction-safe pattern: compute the next absolute deadline off the live state,
294
+ * push it to `ctx.storage.setAlarm`, and — when the DO `alarm()` lifecycle hook
295
+ * fires — map that fire back to a Msg, then re-arm off the post-fire state. The
296
+ * `raft/do.ts` election/heartbeat deadline does it (`rearmAlarm` + `onAlarm`),
297
+ * and the vortex arena tick (`room.ts` `armTick` + `onTick`) replicates the
298
+ * exact shape for the 18 Hz game clock. The two old `do_alarm`/`do_ws` Subs were
299
+ * dropped (see `./index`'s historical note) because `toMachine` fixes the agent
300
+ * machine's Sub type to `DeadlineSub`, so they could never union into a grain's
301
+ * Sub set — leaving the alarm re-arm a HOST concern with no first-class home.
302
+ *
303
+ * {@link durableTimer} is that home: a tiny, transport- and Msg-agnostic helper
304
+ * that owns the three repeated responsibilities and nothing else —
305
+ *
306
+ * 1. **Compute-next-deadline → `setAlarm`.** {@link DurableTimer.rearm} calls
307
+ * the injected {@link DurableTimerConfig.nextDeadline}, and arms the alarm
308
+ * at the returned absolute epoch-ms target.
309
+ * 2. **Idle-when-no-work.** `nextDeadline` returns `null` when there is nothing
310
+ * to schedule (an empty room, a node with no pending election/heartbeat),
311
+ * and `rearm` then arms NOTHING — the DO is allowed to go idle / hibernate
312
+ * instead of spinning the alarm forever. Re-arming is a positive act gated
313
+ * on real work, never an unconditional reschedule.
314
+ * 3. **Fire → Msg → re-arm.** {@link DurableTimer.onAlarm} is the body the DO
315
+ * `alarm()` hook calls: it runs the injected {@link DurableTimerConfig.onFire}
316
+ * (the consumer dispatches its timer Msg there — a `Tick`, a re-derived
317
+ * deadline Msg) and THEN re-arms off the now-advanced state. Re-arm always
318
+ * follows the fire, so a still-active grain keeps ticking and an idle one
319
+ * stops by construction.
320
+ *
321
+ * **Eviction-safe across hibernation.** A DO alarm is the one timer that
322
+ * survives eviction (an isolate `setInterval` dies the moment the isolate
323
+ * sleeps), so the same `durableTimer` re-armed on a cold wake resumes the
324
+ * schedule that a never-evicted instance would have held. The recovery contract
325
+ * is just: after the host rebuilds state (`store.load()` → `replay`), call
326
+ * {@link DurableTimer.rearm} once — it recomputes `nextDeadline` off the
327
+ * recovered state and re-arms (or idles) identically to the live path. The timer
328
+ * holds no in-memory schedule of its own; the deadline is always a pure function
329
+ * of the persisted state, which is what makes the cold-wake re-arm correct.
330
+ *
331
+ * **Dependency-injected / testable.** The alarm arrives as a {@link AlarmStorage}
332
+ * port — the SAME minimal `setAlarm` slice `stepHost` already takes (a real
333
+ * `DurableObjectStorage` satisfies it structurally; a test supplies a fake with
334
+ * no live Workers runtime), mirroring how `event-sourced-store.ts` takes its
335
+ * storage. `nextDeadline` and `onFire` are injected thunks, so the whole timer
336
+ * unit-tests with a faked alarm + a faked clock and no `cloudflare:workers`
337
+ * import. See `.patterns/tea-do/recovery.md` for the cold-wake replay contract
338
+ * the re-arm rides on.
339
+ */
340
+
341
+ /**
342
+ * The construction inputs for {@link durableTimer} — the impure edges the grain
343
+ * injects, kept out of the pure reducer exactly like raft/do's
344
+ * `RaftGrainCtx`/room's `ArenaPorts`.
345
+ */
346
+ interface DurableTimerConfig {
347
+ /**
348
+ * The DO alarm carrier. `setAlarm(atMs)` schedules the next fire at an
349
+ * absolute epoch-ms target. A real `DurableObjectStorage` satisfies this
350
+ * structurally; a test supplies a fake. Only `setAlarm` is read — never the
351
+ * whole `DurableObjectStorage`.
352
+ */
353
+ readonly alarm: AlarmStorage;
354
+ /**
355
+ * Compute the next absolute deadline (epoch ms) off the live state, or `null`
356
+ * when there is no work to schedule. Returning `null` is the idle signal:
357
+ * {@link DurableTimer.rearm} arms nothing and the DO is free to hibernate.
358
+ *
359
+ * Called fresh on every `rearm` (including the post-fire re-arm and the
360
+ * cold-wake re-arm), so it MUST read live state / the injected clock each call
361
+ * — that freshness is what keeps the schedule a pure function of the persisted
362
+ * state and the re-arm eviction-safe. Examples:
363
+ * - raft: `node.subs(state, now())[0]?.atMs ?? null` (election XOR heartbeat).
364
+ * - vortex: `connectionCount() > 0 ? now() + TICK_INTERVAL_MS : null`.
365
+ */
366
+ nextDeadline(): number | null | Promise<number | null>;
367
+ /**
368
+ * Run the timer's effect when the alarm fires — the consumer dispatches its
369
+ * timer Msg here (a `Tick`, a re-derived deadline Msg) and persists/broadcasts
370
+ * as its grain requires. {@link DurableTimer.onAlarm} awaits this BEFORE it
371
+ * re-arms, so the re-arm sees the post-fire state.
372
+ */
373
+ onFire(): void | Promise<void>;
374
+ }
375
+ /**
376
+ * The activated durable timer. Two methods, both idempotent to call from the
377
+ * host edge:
378
+ * - {@link DurableTimer.rearm} — recompute + (re-)arm or idle. Call it once
379
+ * after cold-wake recovery, and after any state transition that can change
380
+ * whether/when the next deadline should fire.
381
+ * - {@link DurableTimer.onAlarm} — the DO `alarm()` body: fire, then re-arm.
382
+ */
383
+ interface DurableTimer {
384
+ /**
385
+ * Recompute {@link DurableTimerConfig.nextDeadline} off the live state and arm
386
+ * the alarm at it — or, when it returns `null`, arm nothing (idle). Safe to
387
+ * call repeatedly; arming the same absolute target twice is a harmless
388
+ * overwrite on the DO alarm API.
389
+ */
390
+ rearm(): Promise<void>;
391
+ /**
392
+ * The body the DO `alarm()` lifecycle hook delegates to: run
393
+ * {@link DurableTimerConfig.onFire}, then {@link DurableTimer.rearm} off the
394
+ * post-fire state. A still-active grain keeps ticking; an idle one (its
395
+ * `nextDeadline` now `null`) stops here.
396
+ */
397
+ onAlarm(): Promise<void>;
398
+ }
399
+ /**
400
+ * Build a {@link DurableTimer} over an injected alarm carrier, a next-deadline
401
+ * computation, and a fire handler. The returned timer owns the
402
+ * compute-next-deadline → `setAlarm` → `alarm()` → fire → re-arm loop and holds
403
+ * no schedule of its own (the deadline is always recomputed from live state),
404
+ * which is what makes it eviction-safe across hibernation.
405
+ */
406
+ declare function durableTimer(config: DurableTimerConfig): DurableTimer;
407
+
408
+ /**
409
+ * @demlik/tea/do — event-sourced persistence mode for a Durable Object actor.
410
+ *
411
+ * `doStore` (sibling, `./index`) is the DEFAULT and stays snapshot-only: it
412
+ * writes the whole `S` blob to one cell on every transition and reads it back
413
+ * on boot. That is byte-for-byte unchanged by this file.
414
+ *
415
+ * `doEventSourcedStore` is the OPT-IN alternative. Instead of (only)
416
+ * checkpointing the latest state, it:
417
+ *
418
+ * 1. APPENDS each applied `Msg` to an append-only log in `DurableObjectStorage`
419
+ * (one cell per event, monotonically keyed).
420
+ * 2. Takes a PERIODIC SNAPSHOT every `snapshotEvery` events (count-based
421
+ * retention — mirrors Akka `RetentionCriteria.snapshotEvery`) so the log
422
+ * that must replay on the next activation is bounded. A grain that needs
423
+ * replay bounded by ELAPSED TIME/TICKS rather than raw event count drives
424
+ * `snapshotNow()` instead (or alongside) — see that method.
425
+ * 3. On activation REBUILDS state by FOLDING the log on top of the latest
426
+ * snapshot. The fold is the substrate's own `replay` — `init(snapshot)`
427
+ * then one reducer step per logged Msg. `replay` returns Cmds as data and
428
+ * never interprets them, so **side effects are suppressed on replay**
429
+ * (recovery.md). This is the same primitive `foldEvents` / the recorder
430
+ * use; it is reused here, not reinvented (ADR 0003: "the fold already
431
+ * exists; wire it to Store").
432
+ *
433
+ * The "recovered/ready" signal (recovery.md `RecoveryCompleted`) is delivered
434
+ * exactly once per activation via `onReady`, EVEN for a fresh actor with an
435
+ * empty log, and is documented to be idempotent (it may fire again on a later
436
+ * activation, so the handler must tolerate repeats).
437
+ *
438
+ * Integration with the cooperating DO:
439
+ *
440
+ * const es = doEventSourcedStore(storage, machine, ctx, {
441
+ * snapshotEvery: 100,
442
+ * parse, // optional boundary parse for the snapshot
443
+ * onReady: (state) => { ... }, // optional, idempotent end-of-recovery hook
444
+ * });
445
+ * const runtime = run(machine, { ctx, store: es.store });
446
+ * // Append every applied Msg to the log. `observe` delivers only applied
447
+ * // Msgs now (#47) — boot has no event, so there is no `null` arm to skip.
448
+ * runtime.observe((msg) => { es.append(msg); });
449
+ * await runtime.ready; // store.load() has folded; onReady has fired once.
450
+ *
451
+ * A time/tick-driven grain (e.g. one that derives simulation ticks between
452
+ * sparse intents) bounds its cold-wake replay by ELAPSED TIME rather than event
453
+ * count by calling `es.snapshotNow()` on its own cadence — every N derived ticks
454
+ * or on a wall-clock interval — so a quiet span that never trips `snapshotEvery`
455
+ * is still snapshotted before its replay tail grows unbounded (#190):
456
+ *
457
+ * // in the grain's tick loop, after the turn's appends have settled:
458
+ * if (tick % SNAPSHOT_EVERY_TICKS === 0) await es.snapshotNow();
459
+ */
460
+
461
+ /**
462
+ * Options for {@link doEventSourcedStore}.
463
+ */
464
+ interface EventSourcedOptions<S, M> {
465
+ /**
466
+ * Take a snapshot every N appended events (count-based retention, mirrors
467
+ * Akka `RetentionCriteria.snapshotEvery`). A snapshot bounds how many events
468
+ * replay on the next activation — it never changes the fold's result, only
469
+ * its length (recovery.md). Must be >= 1. Default 100.
470
+ */
471
+ readonly snapshotEvery?: number;
472
+ /**
473
+ * Boundary parse for the persisted SNAPSHOT cell (the `S` blob). Same
474
+ * contract as `doStore`'s `parse`: returns `S` on a recognized shape, `null`
475
+ * on an unrecognized one (boot fresh), and must NOT throw. Default accepts
476
+ * any non-null, non-array object as `S`.
477
+ */
478
+ readonly parse?: (raw: unknown) => S | null;
479
+ /**
480
+ * Boundary parse for a persisted EVENT (one logged `Msg`). Returns the `M` on
481
+ * a recognized shape; returning `null` DROPS the event from replay (use for a
482
+ * removed Msg variant). Must NOT throw. Default accepts any object carrying a
483
+ * string `type` as `M`.
484
+ */
485
+ readonly parseEvent?: (raw: unknown) => M | null;
486
+ /**
487
+ * End-of-recovery hook (recovery.md `RecoveryCompleted`). Fires EXACTLY ONCE
488
+ * per activation, after the fold completes, carrying the recovered state —
489
+ * even when the log was empty (fresh actor). It may fire again on a later
490
+ * activation, so it MUST be idempotent. Side effects that belong "once after
491
+ * recovery" go here, never in the reducer (the reducer re-runs on every
492
+ * replay).
493
+ */
494
+ readonly onReady?: (state: S) => void;
495
+ /** Cell-key overrides (rarely needed; for coexisting actors in one DO). */
496
+ readonly keys?: {
497
+ readonly snapshot?: string;
498
+ readonly meta?: string;
499
+ readonly eventPrefix?: string;
500
+ };
501
+ }
502
+ /**
503
+ * One persisted log entry handed to a {@link EventSourcedStore.readEvents}
504
+ * consumer: the monotonic `seq` the event was appended under, paired with the
505
+ * decoded `event`. `seq` is the store's own append counter (the key the event
506
+ * lives under), so it both orders the stream and bounds an {@link EventLogRange}.
507
+ */
508
+ interface PersistedEvent<M> {
509
+ /** The append sequence number this event was logged under (1-based, gap-free). */
510
+ readonly seq: number;
511
+ /** The decoded `Msg`, parsed through the store's `parseEvent` boundary. */
512
+ readonly event: M;
513
+ }
514
+ /**
515
+ * Optional inclusive bounds for {@link EventSourcedStore.readEvents}. Both ends
516
+ * are inclusive and match against the event's `seq`; omitting an end leaves it
517
+ * unbounded. `{ fromSeq: 5, toSeq: 10 }` yields seqs 5..10; `{}` (or no
518
+ * argument) yields the whole log.
519
+ */
520
+ interface EventLogRange {
521
+ /** Lowest `seq` to include (inclusive). Default: the first event. */
522
+ readonly fromSeq?: number;
523
+ /** Highest `seq` to include (inclusive). Default: the last event. */
524
+ readonly toSeq?: number;
525
+ }
526
+ /**
527
+ * The handle returned by {@link doEventSourcedStore}: a `Store<S>` to hand to
528
+ * `run(...)`, plus the append + recovery surface the cooperating DO drives.
529
+ */
530
+ interface EventSourcedStore<S, M> {
531
+ /**
532
+ * The `Store<S>` to pass as `run(machine, { ctx, store })`. Its `load()`
533
+ * performs the fold (snapshot + log replay); its `save(state)` is the
534
+ * count-based snapshot writer. `migrate` forwards to `parse`.
535
+ */
536
+ readonly store: Store<S>;
537
+ /**
538
+ * Append one applied `Msg` to the log. Call from `runtime.observe` for every
539
+ * non-null msg. Resolves once the event is durably written. Triggers a
540
+ * snapshot when the event count crosses a `snapshotEvery` boundary.
541
+ */
542
+ append(msg: M): Promise<void>;
543
+ /**
544
+ * Take a snapshot NOW, independent of the count-based `snapshotEvery` trigger
545
+ * (a time/tick-based retention trigger, #190). The count-based trigger bounds
546
+ * the replay tail by raw EVENT COUNT; a grain that derives many state
547
+ * transitions per logged event (e.g. simulation ticks between sparse intents)
548
+ * needs the tail bounded by ELAPSED TIME instead — so it calls `snapshotNow()`
549
+ * on its own cadence (every N derived ticks, or on a wall-clock interval) to
550
+ * checkpoint the current folded state without injecting filler events purely
551
+ * to advance the counter.
552
+ *
553
+ * It checkpoints the latest state the substrate handed `save()` at the current
554
+ * highest sequence, then advances the count-based baseline (`snapshotEvery`
555
+ * counts afresh from here, so a manual snapshot defers the next automatic one).
556
+ *
557
+ * Returns `true` if a snapshot was written, `false` on a no-op: nothing has
558
+ * been applied yet (fresh actor), or no new event exists since the last
559
+ * snapshot (already current). Like the count-based path, it pairs the held
560
+ * state with the highest sequence, so the caller MUST invoke it between
561
+ * settled transitions — after the turn's `append()`s have resolved — never
562
+ * mid-transition (the same boundary discipline `append`'s own snapshot relies
563
+ * on). Resolves once the snapshot is durably written.
564
+ */
565
+ snapshotNow(): Promise<boolean>;
566
+ /**
567
+ * Stream the persisted event log in `seq` order, optionally bounded by an
568
+ * inclusive {@link EventLogRange}. This is the PUBLIC read side of the
569
+ * event-sourced write model — the surface replay / projection / audit
570
+ * consumers (CQRS reads, recovery.md / projections.md) read through, instead
571
+ * of mirroring the store's private `@@es/evt/` key convention.
572
+ *
573
+ * The log is APPEND-ONLY and is never truncated by snapshotting (a snapshot
574
+ * only bounds the *replay* tail, never the log), so this yields EVERY event
575
+ * in range — including ones a snapshot already folds. Each entry carries its
576
+ * `seq`, so a consumer can resume from a known offset (`{ fromSeq }`) or read
577
+ * a window (`{ fromSeq, toSeq }`). An empty (or fully out-of-range) log yields
578
+ * nothing. Events whose `parseEvent` returns `null` (a retired Msg variant)
579
+ * are dropped, identical to the recovery fold.
580
+ *
581
+ * Read-only: it never appends, snapshots, or mutates store state, so streaming
582
+ * the log can never perturb the live actor.
583
+ */
584
+ readEvents(range?: EventLogRange): AsyncIterable<PersistedEvent<M>>;
585
+ }
586
+ /**
587
+ * Build an event-sourced `Store<S>` over `DurableObjectStorage`.
588
+ *
589
+ * The substrate calls `store.load()` once at boot — that is where the fold
590
+ * happens — and `store.save(state)` after every transition — that is where the
591
+ * count-based snapshot is taken. The cooperating DO additionally calls
592
+ * `append(msg)` from `runtime.observe` to grow the log.
593
+ *
594
+ * `machine` and `ctx` are required because the fold is `replay(machine, {
595
+ * loaded: snapshot, msgs: log, ctx })` — the same pure reducer the live runtime
596
+ * uses, so the rebuilt state is identical to the never-evicted state by
597
+ * construction. `ctx` is only used to satisfy `init`'s signature during the
598
+ * fold; an event-sourced machine's `init` rehydrate branch is a pure
599
+ * passthrough (TEA invariant 2), so the fold touches nothing in `ctx`.
600
+ */
601
+ declare function doEventSourcedStore<S, M extends {
602
+ type: string;
603
+ }, Ctx, C extends Cmd = Cmd, U extends Sub = Sub>(storage: DurableObjectStorage, machine: Machine<S, M, C, U, Ctx>, ctx: Ctx, opts?: EventSourcedOptions<S, M>): EventSourcedStore<S, M>;
604
+
605
+ /**
606
+ * @demlik/tea/do — CQRS projections as a first-class seam.
607
+ *
608
+ * TODAY (`./host`), "Model → views" is ad-hoc: `sseHub` is a hand-rolled
609
+ * sink-set fanning `runtime.observe` events out. It re-derives a common shape:
610
+ * subscribe to `observe`, fold each update into a private view, push the view
611
+ * at a sink. (The run's terminal output was once another such observe-driven
612
+ * read model — `captureLastTurn` — but that is now modelled first-class on the
613
+ * write model itself as `state.output` / `runtime.result()`, #46.)
614
+ *
615
+ * This file names that shape. A `Projection<Model, View>` is the canon's
616
+ * "one write model, many projections" contract made concrete on the tea
617
+ * substrate (`.patterns/tea-do/projections.md`):
618
+ *
619
+ * 1. ONE WRITE MODEL, MANY PROJECTIONS. The write model is the agent's `run()`
620
+ * State (`Model`) plus its `Msg` stream. Each projection is an INDEPENDENT
621
+ * `(events|Model) → view` derivation into its OWN read model, with its own
622
+ * id-scoped offset. Adding a projection never touches the write model or
623
+ * any other projection — it only registers another consumer of `observe`.
624
+ *
625
+ * 2. OFFSET-RESUME + IDEMPOTENT APPLY ARE ONE UNIT. A durable projection
626
+ * tracks an EXCLUSIVE offset — the last applied position. On restart it
627
+ * resumes STRICTLY AFTER the stored offset (never ±1); at-least-once
628
+ * delivery means the same update may be re-presented, so `apply` must be
629
+ * idempotent (an upsert of the derived view, never a blind increment).
630
+ * WHEN the offset is committed relative to the view write IS the delivery
631
+ * semantic (`.patterns/tea-do/delivery-semantics.md`):
632
+ * - commit AFTER the view write → at-least-once (may reprocess)
633
+ * - commit in the SAME step → exactly-once (the default here)
634
+ * We persist offset-with-view as one unit (a single `OffsetView` cell), so
635
+ * a durable projection is exactly-once by construction; the at-least-once
636
+ * replay window is still exercised because `drive()` may re-present an
637
+ * update at/below the stored offset (boot reconcile, double-observe), and
638
+ * the exclusive-offset guard makes that a no-op.
639
+ *
640
+ * 3. A VIEW IS ALWAYS REBUILDABLE. Clearing the view and resetting the offset
641
+ * to the start (`rebuild`) replays the `Msg`/`Model` stream from zero. The
642
+ * live view and the rebuilt view are equal by construction — both are the
643
+ * same pure `apply` fold over the same ordered updates.
644
+ *
645
+ * Relationship to `./host`: `sseHub` is re-expressed here as ONE projection
646
+ * (`sseProjection`) WITHOUT changing its public API — `host.sseHub()` keeps
647
+ * working for existing callers (it is the volatile, offset-free case: a live
648
+ * SSE stream has no durable read model to resume).
649
+ *
650
+ * Relationship to `./event-sourced-store`: a durable projection resumes over
651
+ * the SAME append-only event log `doEventSourcedStore` (#68) writes. The fold
652
+ * is the projection's `apply`, scoped to the projection's offset — independent
653
+ * of the write model's snapshot/replay. This file does NOT import the store; it
654
+ * is offset-shaped so a consumer can wire either the live `observe` stream or a
655
+ * replay over the log into the same `apply`.
656
+ */
657
+ /** A projection's identity: `name` (the view) + `key` (the instance). */
658
+ interface ProjectionId {
659
+ readonly name: string;
660
+ readonly key: string;
661
+ }
662
+ /** Render a `ProjectionId` to its canonical `name-key` string. */
663
+ declare function projectionIdString(id: ProjectionId): string;
664
+ /**
665
+ * One unit the projection driver presents to a projection's `apply`. Mirrors
666
+ * the `(msg, model)` pair `runtime.observe` fans out, plus the exclusive
667
+ * `offset` the driver assigns. `msg === null` is the boot update (no event).
668
+ */
669
+ interface ProjectionUpdate<Model, Msg> {
670
+ /** The applied event, or `null` on the boot update (initial state). */
671
+ readonly msg: Msg | null;
672
+ /** The post-transition write-model state. */
673
+ readonly model: Model;
674
+ /**
675
+ * The exclusive position of this update in the ordered stream — monotonic,
676
+ * 1-based; 0 is the boot update (the `NoOffset` start). The stored offset is
677
+ * EXCLUSIVE: re-presenting an update whose offset is `<=` the stored offset
678
+ * must be a no-op (the at-least-once replay window). Do NOT ±1 it.
679
+ */
680
+ readonly offset: number;
681
+ }
682
+ /**
683
+ * A named CQRS projection: an independent fold of the write model's
684
+ * `(Msg|Model)` stream into a private `View`, plus a sink to publish it.
685
+ *
686
+ * @typeParam Model The write-model state (`run()`'s `S`).
687
+ * @typeParam Msg The write-model event (`run()`'s `M`).
688
+ * @typeParam View The read model this projection derives.
689
+ */
690
+ interface Projection<Model, Msg extends {
691
+ type: string;
692
+ }, View> {
693
+ /** This projection's id (offset-isolation unit). */
694
+ readonly id: ProjectionId;
695
+ /** The seed view for a fresh or rebuilt run (the `NoOffset` start). */
696
+ readonly initial: View;
697
+ /**
698
+ * The per-envelope reducer: derive the next view from the previous view and
699
+ * one update. Pure; MUST be idempotent — re-applying an update at the same
700
+ * offset yields the same view (upsert the derived value, never a blind
701
+ * increment). Returning the SAME view reference (or an equal one) for an
702
+ * update the view does not care about is the canonical "skip".
703
+ */
704
+ apply(view: View, update: ProjectionUpdate<Model, Msg>): View;
705
+ /**
706
+ * The sink — publish the derived view. SSE frame, durable cell, report row.
707
+ * Fired by the driver after `apply` produces a new view. A throwing `emit` is
708
+ * isolated by the driver (it must not strand sibling projections).
709
+ */
710
+ emit(view: View): void;
711
+ }
712
+ /**
713
+ * A live, running projection instance: the projection's current `View`, its
714
+ * exclusive stored `offset`, and the `present`/`reset` operations the driver
715
+ * (or a rebuild) calls.
716
+ */
717
+ interface ProjectionRunner<Model, Msg extends {
718
+ type: string;
719
+ }, View> {
720
+ readonly id: ProjectionId;
721
+ /** The current derived view (the read model). */
722
+ view(): View;
723
+ /**
724
+ * The exclusive stored offset — the position of the LAST applied update.
725
+ * Resume reads strictly after this; `0` is the `NoOffset` start.
726
+ */
727
+ offset(): number;
728
+ /**
729
+ * Present one update to the projection. At-least-once entry point:
730
+ * - if `update.offset <= storedOffset` the update was already applied →
731
+ * NO-OP (idempotent replay window; offset stays, view stays, no emit).
732
+ * - otherwise fold via `apply`, advance the offset TO `update.offset`, and
733
+ * `emit` the new view. Offset-and-view advance together (one unit) →
734
+ * exactly-once: the stored offset always matches the folded view.
735
+ * Returns true iff the update was applied (false on the no-op replay).
736
+ */
737
+ present(update: ProjectionUpdate<Model, Msg>): boolean;
738
+ /**
739
+ * Clear the view to `initial` and the offset to 0 — the canonical rebuild
740
+ * reset (offset-tracking.md: "resetting it to NoOffset is the view-rebuild
741
+ * operation"). After `reset`, re-presenting the full stream rebuilds the view.
742
+ */
743
+ reset(): void;
744
+ }
745
+ /**
746
+ * Build a {@link ProjectionRunner} for a projection, starting from a stored
747
+ * offset (default 0 = `NoOffset`, a fresh run). The view is seeded to
748
+ * `projection.initial`; the caller resumes by presenting updates strictly after
749
+ * `startOffset` (the driver's offset guard enforces exclusivity even if an
750
+ * already-applied update is re-presented).
751
+ *
752
+ * `emitOnStart`/`emitOnReset` default false: a fresh runner holds `initial` but
753
+ * does not push it at the sink until the first real update lands (an SSE client
754
+ * that just connected wants the next event, not a replay of the seed).
755
+ */
756
+ declare function runProjection<Model, Msg extends {
757
+ type: string;
758
+ }, View>(projection: Projection<Model, Msg, View>, startOffset?: number): ProjectionRunner<Model, Msg, View>;
759
+ /**
760
+ * A driver over a set of projections sharing one write-model stream. Holds the
761
+ * monotonic offset counter; presents each observed update to every registered
762
+ * runner independently.
763
+ */
764
+ interface ProjectionRegistry<Model, Msg extends {
765
+ type: string;
766
+ }> {
767
+ /** Register a projection; returns its live runner. */
768
+ register<View>(projection: Projection<Model, Msg, View>, startOffset?: number): ProjectionRunner<Model, Msg, View>;
769
+ /**
770
+ * Present one update to EVERY registered runner. Each folds independently; an
771
+ * `apply`/`emit` throw in one runner is caught so it cannot strand the others
772
+ * (errors-are-data: a broken projection is dropped from THIS update, not
773
+ * allowed to corrupt siblings). Returns the offset assigned to this update.
774
+ */
775
+ dispatch(msg: Msg | null, model: Model): number;
776
+ /** The current monotonic offset (the last dispatched update's position). */
777
+ offset(): number;
778
+ /** The registered runners (introspection / tests). */
779
+ runners(): readonly ProjectionRunner<Model, Msg, unknown>[];
780
+ }
781
+ /** Build an empty {@link ProjectionRegistry}. */
782
+ declare function projectionRegistry<Model, Msg extends {
783
+ type: string;
784
+ }>(): ProjectionRegistry<Model, Msg>;
785
+ /**
786
+ * Wire a {@link ProjectionRegistry} to a runtime's transition stream. The boot
787
+ * update (the initial state, offset 0) is presented via `onBoot`; every applied
788
+ * transition (offset 1, 2, …) via `observe` — together they reproduce the
789
+ * "every transition including boot" stream the single `observe(msg | null)`
790
+ * channel used to carry, now that `observe`'s `msg` is total (#47). The registry
791
+ * still assigns its monotonic exclusive offset (boot → 0, first Msg → 1).
792
+ * Returns a cleanup that detaches both subscriptions.
793
+ *
794
+ * Typed structurally against `{ observe, onBoot }` so it accepts any
795
+ * `Runtime<Model, Msg>` without importing the substrate's `Runtime` (types-only
796
+ * boundary).
797
+ */
798
+ declare function driveProjections<Model, Msg extends {
799
+ type: string;
800
+ }>(registry: ProjectionRegistry<Model, Msg>, runtime: {
801
+ observe(observer: (msg: Msg, model: Model) => void): () => void;
802
+ onBoot(handler: (model: Model) => void): () => void;
803
+ }): () => void;
804
+ /**
805
+ * Rebuild a projection's view by folding an ordered event stream from the
806
+ * `NoOffset` start. The runner is seeded to `projection.initial` (the implicit
807
+ * boot / `NoOffset` state), then each `updates[i]` is presented at offset
808
+ * `i + 1` — so the FIRST event (`updates[0]`) lands at offset 1 and is folded,
809
+ * exactly mirroring the live driver, which assigns the first applied `Msg`
810
+ * offset 1 (the boot observe is offset 0 and never enters this list). Returns
811
+ * the rebuilt runner whose `view()` equals the live runner's view for the same
812
+ * ordered stream.
813
+ *
814
+ * Pass the ordered events only; do NOT prepend a boot (`msg === null`) entry.
815
+ * With the `i + 1` offsets a leading boot entry would be applied at offset 1
816
+ * like any event (the seed already covers the `NoOffset` start), not skipped —
817
+ * the offset-0 skip that drops the boot is the live driver's, not this fold's.
818
+ *
819
+ * This is the rebuild-equivalence oracle: a view cleared and replayed from the
820
+ * stream equals the live view.
821
+ */
822
+ declare function rebuildProjection<Model, Msg extends {
823
+ type: string;
824
+ }, View>(projection: Projection<Model, Msg, View>, updates: readonly {
825
+ readonly msg: Msg | null;
826
+ readonly model: Model;
827
+ }[]): ProjectionRunner<Model, Msg, View>;
828
+
829
+ /**
830
+ * @demlik/tea/do — the minimum-viable Durable-Object HOST for a `@demlik/tea`
831
+ * `createAgent` runtime.
832
+ *
833
+ * `doStore` (in `./index`) is the durability half. This file is the rest of the
834
+ * boilerplate a DO consumer hand-rolls every time it drives a tea agent over a
835
+ * remote transport: the deferred-tool gateway, auto-boot on rehydrate, the
836
+ * command-runner WS accept, the runtime→SSE plumbing, and a clean read of a
837
+ * run's terminal output. Each export below names exactly the hand-rolled piece
838
+ * it deletes from the consumer.
839
+ *
840
+ * ## The transport model: ONE Sub type, gateway-bridged I/O
841
+ *
842
+ * `createAgent().toMachine()` fixes the machine's Sub type to `DeadlineSub` —
843
+ * the agent owns the retry + watchdog timers and nothing else. That is the
844
+ * WHOLE Sub model for a DO-hosted agent, and it is coherent because the
845
+ * transport deliberately does NOT live in the Sub system:
846
+ *
847
+ * - Inbound WebSocket frames are bridged straight into `gateway.settle(...)`
848
+ * from the DO's `webSocketMessage` lifecycle method — a direct dispatch,
849
+ * not a Sub.
850
+ * - The per-tool deadline is a `setTimeout` inside the gateway — a timer, not
851
+ * a DO alarm.
852
+ *
853
+ * The deferred-tool gateway owns each tool round-trip as a Promise the interpret
854
+ * cell awaits; the transport bridges results into `dispatch` from outside the
855
+ * Sub system. The agent's Sub type stays exactly `DeadlineSub` — there is no
856
+ * competing DO-native Sub variant to union in, and the host needs none. This is
857
+ * the single transport model for `@demlik/tea/do` (#52); see
858
+ * `.patterns/tea/durable-actors.md` for the host-layer north star.
859
+ */
860
+
861
+ /**
862
+ * The deferred-tool gateway. `R` is the consumer's tool-result type (what
863
+ * `agent_tool_ok` carries).
864
+ *
865
+ * - `await(callId, send, deadlineMs)` — register the round-trip, fire `send`,
866
+ * return the Promise the reply (or the deadline) settles. Idempotent per
867
+ * `callId`: a second call for an in-flight id returns the SAME promise
868
+ * without re-firing `send` (the boot-reconcile re-fire is a no-op at the
869
+ * gateway, mirroring the agent's idempotent boot).
870
+ * - `settle(callId, result)` — the transport calls this on a reply; resolves
871
+ * the awaited promise. A settle for an unknown / already-settled id is a
872
+ * no-op (the callId guard the consumer used to keep in its pending Map).
873
+ * - `fail(callId, reason)` — settle the round-trip as an error on demand
874
+ * (e.g. a transport-level close). Same unknown-id no-op.
875
+ * - `inFlight()` — the callIds with an open round-trip (tests / introspection).
876
+ *
877
+ * The deadline → reject path is what makes "timeout is data": the tool cell's
878
+ * `catch` maps the rejection to `agent_tool_err`, recorded on the conversation,
879
+ * never a hang.
880
+ */
881
+ interface DeferredGateway<R> {
882
+ await(callId: string, send: () => void, deadlineMs: number): Promise<R>;
883
+ settle(callId: string, result: R): void;
884
+ fail(callId: string, reason: string): void;
885
+ inFlight(): readonly string[];
886
+ }
887
+ /**
888
+ * Build a deferred-tool gateway. No clock / RNG captured at construction — the
889
+ * deadline is per-call (`await(..., deadlineMs)`), so one gateway serves every
890
+ * tool family with its own timeout.
891
+ *
892
+ * Deletes from the consumer: the `pending` Map, the `Pending` interface, the
893
+ * `awaitClient` method, the `forceTimeout` plumbing, and the `deliverResult`
894
+ * callId-guard body — all of it collapses to this object plus the tool cell's
895
+ * `await` + try/catch.
896
+ */
897
+ declare function deferredGateway<R>(): DeferredGateway<R>;
898
+ /** A `callId`-keyed durable round-trip: maps the gateway's string id to the
899
+ * ledger's monotonic delivery id so a `settle(callId)` finds the owed entry. */
900
+ interface DurableDeferredGateway<R> extends DeferredGateway<R> {
901
+ /** The ledger recorder this gateway feeds (for `surviving()` on activation). */
902
+ readonly recorder: PendingEffectsRecorder<{
903
+ callId: string;
904
+ }>;
905
+ }
906
+ /**
907
+ * Wrap a `DeferredGateway<R>` so every round-trip is also recorded in a durable
908
+ * `PendingEffectsRecorder`. The in-memory gateway is the fast path; the recorder
909
+ * is the durability slice.
910
+ *
911
+ * - `recordOwed(callId, event)` — fires from `await`, BEFORE `send`, carrying
912
+ * the `effect_owed` event the consumer must persist (then deliver).
913
+ * - `recordConfirmed(callId, event)` — fires from `settle`/`fail` for a known
914
+ * id, carrying the `effect_confirmed` event to persist.
915
+ * - `reissue(callId, send, deadlineMs)` — re-arm a surviving round-trip on
916
+ * activation. Idempotent: the gateway's own `await` returns the open promise
917
+ * without re-firing `send` for an in-flight `callId`, so re-issuing an id
918
+ * that is somehow still pending is a no-op (mirrors the boot-reconcile
919
+ * re-fire). The effect is already owed, so no second `effect_owed` is
920
+ * recorded.
921
+ *
922
+ * The effect payload the ledger stores is the `callId` (the correlation key the
923
+ * consumer round-trips); the consumer maps it back to its tool-call context.
924
+ */
925
+ declare function durableDeferredGateway<R>(inner: DeferredGateway<R>, recorder: PendingEffectsRecorder<{
926
+ callId: string;
927
+ }>, hooks: {
928
+ recordOwed(callId: string, event: EffectOwed<{
929
+ callId: string;
930
+ }>): void;
931
+ recordConfirmed(callId: string, event: EffectConfirmed): void;
932
+ }): DurableDeferredGateway<R>;
933
+ /**
934
+ * The typed cold-wake resume port `bootResume` fires through — the agent's
935
+ * `AgentBootPort` (issue #60) generalized to any DO-hosted machine (issue #231).
936
+ *
937
+ * - `isResumable(state)` — true iff the rehydrated State is mid-loop and needs a
938
+ * resume dispatch. False on a fresh boot, which makes `bootResume` a no-op.
939
+ * - `resumeMsg(now)` — the TYPED constructor for the single resume Msg (never a
940
+ * hand-built `{ type: … }` literal); the `agentBootMsg` shape lifted out so a
941
+ * non-agent grain supplies its own boot Msg constructor.
942
+ */
943
+ interface ResumePort<S, M extends {
944
+ type: string;
945
+ }> {
946
+ readonly isResumable: (state: S) => boolean;
947
+ readonly resumeMsg: (now: number) => M;
948
+ }
949
+ /**
950
+ * After `runtime.ready`, derive the single resume Msg from the rehydrated State
951
+ * via `port` and dispatch it exactly once — the generalized AgentBoot. On a
952
+ * fresh (non-rehydrated) machine `port.isResumable` is false and this is a
953
+ * no-op. Call once right after building the runtime; it standardizes the
954
+ * hand-written `if (isResumable(state)) await runtime.dispatch(bootMsg(now()))`
955
+ * every DO-hosted machine otherwise re-derives.
956
+ *
957
+ * `await ready` is the single boot gate (issue #45): `getState()` is total only
958
+ * on the booted `Runtime`, so state cannot be inspected before boot populates
959
+ * it. `now` is injected (the sole boot clock read) so a test can pin it.
960
+ */
961
+ declare function bootResume<S, M extends {
962
+ type: string;
963
+ }, E extends {
964
+ type: string;
965
+ } = never>(booting: BootingRuntime<S, M, E>, port: ResumePort<S, M>, now?: () => number): Promise<void>;
966
+ /**
967
+ * True iff an agent slice loaded from storage is mid-loop (running + awaiting
968
+ * tools) — the resumable case `agent_boot` re-fires.
969
+ *
970
+ * The resumability question IS "is the run suspended on tools?" — so this
971
+ * delegates to the agent's own typed `status` channel (issue #49) rather than
972
+ * re-deriving the private `run.phase` / `conversation.awaiting` shape by hand.
973
+ * The host no longer leaks the agent's internal slice structure; a change to
974
+ * `Awaiting` surfaces as a compile error in `status`, not a silent break here.
975
+ */
976
+ declare function agentIsResumable<Stage, P extends string, O extends Record<P, unknown>, R>(state: AgentState<Stage, P, O, R>): boolean;
977
+ /**
978
+ * The AGENT specialization of {@link bootResume} (issue #231): after
979
+ * `runtime.ready`, self-dispatch `agent_boot` iff the rehydrated slice is
980
+ * resumable, a no-op on a fresh DO. Wires the agent slice's `agentIsResumable`
981
+ * predicate + the agent-owned `agentBootMsg` typed port (issue #60) into the
982
+ * generalized helper, so the resume dispatch lives in ONE place — see
983
+ * `bootResume`'s docblock for the boot-gate / init-purity rationale.
984
+ *
985
+ * `now` is injected (the host's only clock read for boot) so a test can pin it.
986
+ */
987
+ declare function autoBoot<Stage, P extends string, O extends Record<P, unknown>, R, E extends {
988
+ type: string;
989
+ } = never>(booting: BootingRuntime<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, E>, now?: () => number): Promise<void>;
990
+ /**
991
+ * Accept a command-runner WebSocket on the DO's `fetch`. Adds the server end to
992
+ * `clients`, wires `message` → `onFrame(data)` (string frames only) and
993
+ * `close` → deregister, and returns the 101 upgrade Response.
994
+ *
995
+ * The consumer's `onFrame` parses the frame and calls `gateway.settle(callId,
996
+ * result)` — the inbound bridge. Deletes the consumer's `acceptClient` method.
997
+ */
998
+ declare function acceptCommandSocket(clients: Set<WebSocket>, onFrame: (data: string) => void): Response;
999
+ /**
1000
+ * Broadcast a JSON frame to every connected command-runner socket. A dead
1001
+ * socket is skipped (dropped on its own `close` event). Lifts the consumer's
1002
+ * fan-out loop in `sendCommand`.
1003
+ */
1004
+ declare function broadcast(clients: Set<WebSocket>, frame: unknown): void;
1005
+ /**
1006
+ * The slice of `DurableObjectState` the durable carrier needs: the Hibernation
1007
+ * API accept + the registry of hibernatable sockets. A real `DurableObjectState`
1008
+ * satisfies this structurally; a test supplies a fake (no live Workers runtime
1009
+ * needed to exercise the accept/registry seam).
1010
+ */
1011
+ interface HibernatableCtx {
1012
+ acceptWebSocket(ws: WebSocket): void;
1013
+ getWebSockets(): WebSocket[];
1014
+ }
1015
+ /**
1016
+ * Accept a command-runner WebSocket using the Cloudflare **Hibernation API** so
1017
+ * the socket survives DO eviction. Unlike {@link acceptCommandSocket}, this does
1018
+ * NOT attach an isolate-resident `message` listener (that listener is what dies
1019
+ * on eviction) — instead `ctx.acceptWebSocket(server)` hands the socket to the
1020
+ * runtime, which re-creates the DO and calls its `webSocketMessage(ws, data)`
1021
+ * lifecycle method on the next inbound frame. The consumer routes that method to
1022
+ * its `onFrame` (which calls `carrier.settle(callId, result)`).
1023
+ *
1024
+ * Returns the 101 upgrade Response. There is no `clients` registry to keep — the
1025
+ * live sockets are `ctx.getWebSockets()` (see {@link broadcastHibernatable}),
1026
+ * which the runtime repopulates after a wake.
1027
+ */
1028
+ declare function acceptDurableCommandSocket(ctx: HibernatableCtx): Response;
1029
+ /**
1030
+ * Broadcast a JSON frame to every hibernatable command-runner socket. The live
1031
+ * set is read from `ctx.getWebSockets()` (the runtime repopulates it after a
1032
+ * wake), NOT a resident `Set<WebSocket>` — so it is correct immediately after an
1033
+ * eviction, before any other lifecycle method has run. A dead socket is skipped.
1034
+ */
1035
+ declare function broadcastHibernatable(ctx: HibernatableCtx, frame: unknown): void;
1036
+ /**
1037
+ * The durable command carrier — a {@link DurableDeferredGateway} whose every
1038
+ * tool round-trip is also a durable owed effect.
1039
+ *
1040
+ * A consumer's tool interpret cell still calls `carrier.await(callId, send,
1041
+ * deadlineMs)` and awaits the Promise (mapping Ok/Err to `agent_tool_ok` /
1042
+ * `agent_tool_err`) — the SAME fast path as the volatile gateway. The addition
1043
+ * is durability: `await` hands the consumer an `effect_owed` event (via
1044
+ * `recordOwed`) to PERSIST before the send, and `settle`/`fail` hand it an
1045
+ * `effect_confirmed` (via `recordConfirmed`) to persist. On the next activation
1046
+ * the consumer reads its persisted ledger events back and calls
1047
+ * {@link reissueSurvivingEffects} to re-fire the survivors.
1048
+ *
1049
+ * It extends `DurableDeferredGateway` (so it is a drop-in for the gateway the
1050
+ * interpret cell already awaits) and exposes the underlying `recorder` for a
1051
+ * host that wants to read `surviving()` directly.
1052
+ */
1053
+ interface DurableCommandCarrier<R> extends DeferredGateway<R> {
1054
+ /** The ledger recorder backing this carrier (for `surviving()` introspection). */
1055
+ readonly recorder: PendingEffectsRecorder<{
1056
+ callId: string;
1057
+ }>;
1058
+ }
1059
+ /**
1060
+ * Build a durable command carrier over a (volatile) `DeferredGateway<R>` and a
1061
+ * `PendingEffectsRecorder`. Thin assembly over {@link durableDeferredGateway}:
1062
+ * it names the #91 piece — "the command carrier whose round-trips survive
1063
+ * eviction" — and keeps the recorder reachable for the activation-time re-emit.
1064
+ *
1065
+ * - `inner` — the volatile gateway whose in-isolate Promise the interpret cell
1066
+ * awaits (typically `deferredGateway<R>()`).
1067
+ * - `recorder` — the durable-effects recorder (typically
1068
+ * `pendingEffectsLedger({ events, lastId })` rehydrated from the persisted
1069
+ * log on a wake, or a fresh `pendingEffectsLedger()` on a cold boot).
1070
+ * - `hooks.recordOwed` / `hooks.recordConfirmed` — the persist callbacks the
1071
+ * consumer points at its event log (so the ledger survives eviction).
1072
+ *
1073
+ * The carrier never persists anything itself (it does not own the consumer's
1074
+ * log) — it hands the consumer the events, faithful to "persist the intent
1075
+ * before deliver".
1076
+ */
1077
+ declare function durableCommandCarrier<R>(inner: DeferredGateway<R>, recorder: PendingEffectsRecorder<{
1078
+ callId: string;
1079
+ }>, hooks: {
1080
+ recordOwed(callId: string, event: EffectOwed<{
1081
+ callId: string;
1082
+ }>): void;
1083
+ recordConfirmed(callId: string, event: EffectConfirmed): void;
1084
+ }): DurableCommandCarrier<R>;
1085
+ /**
1086
+ * RE-EMIT ON ACTIVATION (the #91 wake path). Given a durable carrier whose
1087
+ * recorder was rehydrated from the persisted ledger events (e.g.
1088
+ * `durableCommandCarrier(deferredGateway(), pendingEffectsLedger({ events }),
1089
+ * …)` on a wake), re-fire every owed-but-unconfirmed round-trip via
1090
+ * `reissue(callId)` — the consumer's "re-send the command frame for this
1091
+ * `callId`" callback. The survivors are read straight off the carrier's own
1092
+ * recorder (`recorder.surviving()`), which folded the restore log at
1093
+ * construction — the carrier is the single source of the in-flight set, so the
1094
+ * caller does not re-fold the log here.
1095
+ *
1096
+ * Idempotent by construction:
1097
+ * - the ledger's monotonic `deliveryId` is the dedup key the RECEIVER uses, so
1098
+ * a late/duplicate reply for an already-resolved round-trip is absorbed
1099
+ * (at-least-once delivery — see `survivingEffects`);
1100
+ * - re-issuing the SAME survivor twice within one activation re-sends once —
1101
+ * if `reissue` routes through the carrier's `await`, the gateway returns the
1102
+ * open Promise for an in-flight `callId` without re-firing `send`.
1103
+ *
1104
+ * Returns the `callId`s re-emitted (oldest-first by id) — the host can log /
1105
+ * assert the re-emit set. On a fresh or fully-confirmed carrier this is empty
1106
+ * (nothing re-fired).
1107
+ *
1108
+ * Call this once, right after the runtime's boot gate resolves (alongside
1109
+ * {@link autoBoot}): `autoBoot` re-fires the agent's own `agent_boot`;
1110
+ * `reissueSurvivingEffects` re-fires the in-flight TOOL round-trips. Together
1111
+ * they restore a suspended run's full in-flight state after a wake.
1112
+ */
1113
+ declare function reissueSurvivingEffects<R>(carrier: DurableCommandCarrier<R>, reissue: (callId: string) => void): readonly string[];
1114
+ /**
1115
+ * A set of SSE sinks plus the plumbing to fan an event out to all of them and
1116
+ * to open a `text/event-stream` Response wired to a fresh sink.
1117
+ *
1118
+ * The consumer:
1119
+ * 1. holds one `SseHub<E>` per DO,
1120
+ * 2. calls `hub.emit(event)` from its Msg→event mapping (driven off
1121
+ * `runtime.observe`), and
1122
+ * 3. returns `hub.open()` from the `/sse` route.
1123
+ *
1124
+ * `register(sink)` exists for tests: subscribe a sink directly and observe the
1125
+ * exact event sequence a live client would receive (a live `text/event-stream`
1126
+ * fights vitest-pool-workers teardown). Deletes the consumer's `sseSinks` set,
1127
+ * `openSse`, `emitSse`, and `onSseEvent`.
1128
+ */
1129
+ interface SseHub<E> {
1130
+ emit(event: E): void;
1131
+ open(): Response;
1132
+ register(sink: (event: E) => void): () => void;
1133
+ }
1134
+ /** Build an SSE hub for event type `E` (the consumer's domain event shape). */
1135
+ declare function sseHub<E>(): SseHub<E>;
1136
+ /**
1137
+ * Express an {@link SseHub} as a {@link Projection}. The projection's view is
1138
+ * the latest emitted SSE event (or `null` before the first); `apply` maps each
1139
+ * write-model update to an event via `toEvent` and emits it through `hub.emit`,
1140
+ * so the SSE stream is driven by the SAME hub `sseHub()` returns — no API
1141
+ * change for the `/sse` route (`hub.open()`) or any existing caller.
1142
+ *
1143
+ * - `toEvent(msg, model)` returns the SSE event for this update, or `null` to
1144
+ * skip it (the view stays, nothing is emitted).
1145
+ * - `name`/`key` default to `"sse"`/`"main"`; override for a second SSE view.
1146
+ *
1147
+ * @example
1148
+ * const hub = sseHub<MyEvent>();
1149
+ * const registry = projectionRegistry<MyState, MyMsg>();
1150
+ * registry.register(sseProjection(hub, (msg) =>
1151
+ * msg?.type === "turn_done" ? { kind: "turn", ... } : null,
1152
+ * ));
1153
+ * driveProjections(registry, runtime); // SSE is now one projection
1154
+ * // /sse route unchanged:
1155
+ * return hub.open();
1156
+ */
1157
+ declare function sseProjection<Model, Msg extends {
1158
+ type: string;
1159
+ }, E>(hub: SseHub<E>, toEvent: (msg: Msg | null, model: Model) => E | null, id?: {
1160
+ name?: string;
1161
+ key?: string;
1162
+ }): Projection<Model, Msg, E | null>;
1163
+ /**
1164
+ * Wire an {@link SseHub} to a runtime's semantic {@link AgentEvent} stream (#47).
1165
+ * For each event `type` the runtime can emit, subscribes via `runtime.on` and
1166
+ * pushes `toFrame(event)` at the hub (returning `null` skips that event). The
1167
+ * runtime MUST have been built with `run(machine, { events: agentEvents() })`,
1168
+ * which is what makes `on` deliver the `AgentEvent`s. Returns a cleanup that
1169
+ * detaches every subscription.
1170
+ *
1171
+ * This is the runtime→SSE adapter expressed over the typed event channel: no
1172
+ * `observe` firehose, no private-Msg coupling. `hub.open()` and every existing
1173
+ * `sseHub` caller are unchanged — only the SOURCE of `hub.emit` moves from a
1174
+ * hand-rolled Msg switch to `on`.
1175
+ *
1176
+ * @example
1177
+ * const hub = sseHub<MyFrame>();
1178
+ * const runtime = await run(machine, { ctx, events: agentEvents() }).ready;
1179
+ * const off = sseFromAgentEvents(runtime, hub, (e) =>
1180
+ * e.type === "RunDone" ? { kind: "done", output: e.output } : null,
1181
+ * );
1182
+ * // /sse route unchanged:
1183
+ * return hub.open();
1184
+ */
1185
+ declare function sseFromAgentEvents<R, Frame>(runtime: {
1186
+ on<K extends AgentEvent<R>["type"]>(type: K, handler: (event: Extract<AgentEvent<R>, {
1187
+ type: K;
1188
+ }>) => void): () => void;
1189
+ }, 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
+ /**
1204
+ * What the consumer supplies to {@link createAgentHost} — the domain mappings,
1205
+ * nothing of the wiring. `Stage`/`P`/`O`/`R` are the agent slice's type
1206
+ * parameters (the consumer names them once); `Frame` is its SSE event shape.
1207
+ */
1208
+ interface AgentHostConfig<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame> {
1209
+ /**
1210
+ * Build the wired agent machine. Called once per host build (per activation),
1211
+ * AFTER the previous runtime — if any — was torn down. The consumer wires its
1212
+ * per-tool interpret here (`agent.toMachine({ toolInterpret })`).
1213
+ */
1214
+ readonly buildMachine: () => Machine<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, any, any, any>;
1215
+ /** The durable `Store` for the agent slice (typically `doStore(storage, parse)`). */
1216
+ readonly store: Store<AgentState<Stage, P, O, R>>;
1217
+ /** The Ctx the machine threads to its interpret cells. */
1218
+ readonly ctx: unknown;
1219
+ /**
1220
+ * The run-terminality predicate (#46) — what makes `result()` first-class.
1221
+ * Defaults to the agent's own terminal phases (`done` / `failed`).
1222
+ */
1223
+ readonly terminal?: (state: AgentState<Stage, P, O, R>) => boolean;
1224
+ /**
1225
+ * Map one semantic {@link AgentEvent} to the consumer's SSE frame, or `null`
1226
+ * to skip it. This is the ONLY place the consumer touches the SSE seam — the
1227
+ * host owns the subscription wiring (`sseFromAgentEvents`) and the hub.
1228
+ */
1229
+ readonly toSseFrame: (event: AgentEvent<R>) => Frame | null;
1230
+ /** Clock injected for `autoBoot` (the host's only boot clock read). */
1231
+ readonly now?: () => number;
1232
+ }
1233
+ /**
1234
+ * The assembled host: the runtime cell + the SSE hub + the framework test seam,
1235
+ * owned ONCE. A DO holds one `AgentHost` and delegates to it; its own surface
1236
+ * shrinks to the domain (the gateway, the WS bridge, the command-send mapping).
1237
+ *
1238
+ * @typeParam Stage Pipeline stage type.
1239
+ * @typeParam P Brain-call purposes.
1240
+ * @typeParam O Per-purpose outputs (bound to {@link AgentTurn}).
1241
+ * @typeParam R Tool result type.
1242
+ * @typeParam Frame The consumer's SSE event shape.
1243
+ */
1244
+ interface AgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame> {
1245
+ /**
1246
+ * Get (or build) the booted runtime. Build-once-boot-once: the first call
1247
+ * builds the machine, wires SSE off the semantic event stream, awaits the boot
1248
+ * gate, runs the `autoBoot` re-fire for a rehydrated suspended run, and caches
1249
+ * the booted `Runtime`; subsequent calls return the cached handle. The single
1250
+ * assembly every consumer used to hand-roll in `getRuntime()`.
1251
+ */
1252
+ runtime(): Promise<Runtime<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, AgentEvent<R>>>;
1253
+ /**
1254
+ * The agent's lifecycle status (#49) — the ONE typed channel a consumer reads
1255
+ * instead of re-deriving `run.phase` / `awaiting` by hand. Ensures the runtime
1256
+ * is built + boot-reconciled first. Replaces the per-consumer `isSuspended` /
1257
+ * `runPhase` test methods (read `status().kind`).
1258
+ */
1259
+ status(): Promise<AgentStatus<Stage>>;
1260
+ /**
1261
+ * The run's terminal State (#46), or `undefined` while in flight. Read the
1262
+ * run's product off it (e.g. `result()?.output`). Replaces the per-consumer
1263
+ * `verdict` plumbing's source. Ensures the runtime is built first.
1264
+ */
1265
+ result(): Promise<AgentState<Stage, P, O, R> | undefined>;
1266
+ /**
1267
+ * The SSE hub — the route (`host.sse.open()`) AND the test seam
1268
+ * (`host.sse.register(sink)`). The host drives `hub.emit` off the semantic
1269
+ * event stream; the consumer never touches it except to open / register.
1270
+ */
1271
+ readonly sse: SseHub<Frame>;
1272
+ /**
1273
+ * Tear down the cached runtime (drain its tail via `stop()`, drop the handle)
1274
+ * so the next `runtime()` rebuilds from storage. The framework `settle` test
1275
+ * seam, owned once.
1276
+ */
1277
+ reset(): Promise<void>;
1278
+ }
1279
+ /**
1280
+ * Build an {@link AgentHost} from the consumer's domain mappings. Owns the
1281
+ * runtime+events+autoBoot assembly and the framework test seam — the wiring
1282
+ * issue #53 had every consumer re-write.
1283
+ *
1284
+ * @example
1285
+ * const host = createAgentHost<Stage, Purpose, Outputs, ClientResult, SseEvent>({
1286
+ * buildMachine: () => agent.toMachine<Ctx>({ toolInterpret }),
1287
+ * store: doStore(storage, parse),
1288
+ * ctx,
1289
+ * toSseFrame: (e) =>
1290
+ * e.type === "ToolSettled" ? { kind: "result", ...} :
1291
+ * e.type === "TurnSettled" && e.turn.toolCalls.length === 0
1292
+ * ? { kind: "verdict", verdict: e.turn.content } :
1293
+ * e.type === "RunDone" ? { kind: "phase", phase: "done" } : null,
1294
+ * });
1295
+ * // route: return host.sse.open();
1296
+ * // start: await (await host.runtime()).dispatch({ type: "agent_start", ... });
1297
+ * // test: (await host.status()).kind === "suspended"
1298
+ */
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>;
1300
+
1301
+ /**
1302
+ * @demlik/tea/do — the idempotent-effect brick (the receiver/effect-side dedup).
1303
+ *
1304
+ * ## The half this closes (do NOT re-implement the ledger)
1305
+ *
1306
+ * The runtime's `stepDispatch` step order is SAVE-BEFORE-EFFECTS: state is
1307
+ * persisted (`store.save(next)`) *before* its commands are interpreted
1308
+ * (`packages/tea/src/index.ts` — "Save-before-effects is the hard ordering;
1309
+ * tests pin it"). So a DO evicted after `save` but before an effect's result is
1310
+ * folded RE-FIRES that effect on the next activation.
1311
+ *
1312
+ * `durable-effects.ts` (sibling) owns the SENDER half of this: the at-least-once
1313
+ * `pendingEffectsLedger`, whose surviving owed entries `reissueSurvivingEffects`
1314
+ * re-fires on activation. Its own header states the contract this file fulfils:
1315
+ * "At-least-once ⇒ a confirmed-but-not-yet-persisted delivery WILL be re-sent
1316
+ * after a crash, so the receiver must deduplicate by id" and "the receiver owns
1317
+ * its own dedup." This module IS that receiver-side dedup — the piece that
1318
+ * upgrades the ledger's at-least-once re-fire to **effectively-once at the
1319
+ * external write**:
1320
+ *
1321
+ * - ledger (`survivingEffects` / `reissueSurvivingEffects`) = at-least-once re-fire;
1322
+ * - this brick (`idempotentEffect` + `appliedEffects`) = effect-side dedup
1323
+ * ⇒ together, effectively-once.
1324
+ *
1325
+ * It COMPLEMENTS the ledger; it does not re-implement it. The ledger keys owed
1326
+ * effects by a monotonic `DeliveryId` (its private sender bookkeeping); this
1327
+ * brick keys applied effects by a caller-supplied durable `EffectKey` (e.g.
1328
+ * `(runId, violationId)`), because dedup identity must be stable across the
1329
+ * re-fire — the same external write must resolve to the same key on every
1330
+ * activation, which a fresh monotonic id cannot guarantee.
1331
+ *
1332
+ * ## The load-bearing rule (mirrors `durable-effects.ts`)
1333
+ *
1334
+ * The applied-marker set is a PURE FOLD over the same kind of event stream the
1335
+ * actor already replays — it is **NOT** a separately-persisted side table:
1336
+ *
1337
+ * - `effect_applied` — `{ key }` ADDS the key to the applied set.
1338
+ * - `effect_forgotten` — `{ key }` REMOVES it (retention — see below).
1339
+ *
1340
+ * `foldApplied(events)` is `events.reduce(applyAppliedEvent, empty)` — the same
1341
+ * fold shape `replay` / `foldLedger` use. This makes the marker live "alongside
1342
+ * State (single authority)" for BOTH persistence modes:
1343
+ *
1344
+ * - `doEventSourcedStore`: persist the two events into the SAME log it already
1345
+ * appends; folding the log on activation rebuilds the applied set for free.
1346
+ * - default snapshot `doStore`: carry the `AppliedEffects` set as a field of
1347
+ * the Model, round-tripped by the store's `serialize`/`parse` hooks (a `Set`
1348
+ * flattens under `JSON.stringify`, exactly the `Map` sharp edge #182), so it
1349
+ * is checkpointed with the rest of `S`.
1350
+ *
1351
+ * Either way the marker is folded/snapshotted with State, never a side channel.
1352
+ *
1353
+ * ## Ordering — why the marker is recorded AFTER `fn` (at-least-once, not at-most)
1354
+ *
1355
+ * `run` invokes `fn` and only THEN yields the `effect_applied` event to persist.
1356
+ * An eviction in the window between `fn` succeeding and that event being folded
1357
+ * re-fires the effect (at-least-once) — correct, because recording the marker
1358
+ * BEFORE `fn` would let a crash strand the marker with the write never made
1359
+ * (at-most-once ⇒ a silently-dropped effect). Once the marker is folded, every
1360
+ * subsequent re-fire of that key is a proven no-op: `fn` is not invoked again.
1361
+ * The ledger guarantees the re-fire; this brick swallows the duplicate.
1362
+ *
1363
+ * ## Retention — pruning mirrors the ledger's confirm-removes-entry (not a new policy)
1364
+ *
1365
+ * An applied-marker is only needed while its effect can still re-fire. The
1366
+ * ledger stops re-firing an effect once its `effect_confirmed` removes the owed
1367
+ * entry — so at that same point the marker is dead weight and is pruned via
1368
+ * `effect_forgotten` (`guard.forget(key)`), keeping the applied set bounded. This
1369
+ * deliberately reuses the ledger's confirm-removes-entry retention shape rather
1370
+ * than inventing a second policy (the #227 triage's one open question).
1371
+ */
1372
+
1373
+ /**
1374
+ * The caller-supplied durable dedup identity for an effect — stable across the
1375
+ * re-fire (e.g. `` `${runId}:${violationId}` ``). Unlike the ledger's monotonic
1376
+ * `DeliveryId`, it is chosen by the domain so the SAME external write resolves
1377
+ * to the SAME key on every activation.
1378
+ */
1379
+ type EffectKey = string;
1380
+ /** An effect keyed `key` has been APPLIED. Adds `key` to the set when folded. */
1381
+ interface EffectApplied extends Cmd<"effect_applied"> {
1382
+ readonly key: EffectKey;
1383
+ }
1384
+ /**
1385
+ * An applied-marker is no longer needed (its effect can no longer re-fire, e.g.
1386
+ * its ledger entry was confirmed) and may be pruned. Removes `key` when folded.
1387
+ */
1388
+ interface EffectForgotten extends Cmd<"effect_forgotten"> {
1389
+ readonly key: EffectKey;
1390
+ }
1391
+ /** The event union the applied-marker set folds over. */
1392
+ type AppliedEffectsEvent = EffectApplied | EffectForgotten;
1393
+ /** The set of keys whose effect has been applied. A pure fold result. */
1394
+ type AppliedEffects = ReadonlySet<EffectKey>;
1395
+ /** The empty applied set — the fold's seed (a fresh actor has applied nothing). */
1396
+ declare function emptyApplied(): AppliedEffects;
1397
+ /**
1398
+ * Apply ONE event — the reducer at the heart of the fold.
1399
+ *
1400
+ * - `effect_applied` → add `key` (a duplicate add is a no-op — a re-fired
1401
+ * already-applied key tolerated by at-least-once).
1402
+ * - `effect_forgotten` → remove `key` (a no-op if already absent).
1403
+ *
1404
+ * Pure: returns a NEW set on change, never mutates the input — so folding the
1405
+ * same events twice (live and post-eviction) yields equal results.
1406
+ */
1407
+ declare function applyAppliedEvent(applied: AppliedEffects, event: AppliedEffectsEvent): AppliedEffects;
1408
+ /**
1409
+ * THE FOLD. Rebuild the applied set from an event stream — the same fold shape
1410
+ * `replay`/`foldLedger` use. After a simulated eviction this is called over the
1411
+ * persisted events and yields a set equal to the never-evicted one, because a
1412
+ * pure fold over identical input is identical. This is what proves "no side
1413
+ * table": the set is `events` reduced, nothing more.
1414
+ */
1415
+ declare function foldApplied(events: Iterable<AppliedEffectsEvent>): AppliedEffects;
1416
+ /** Has this key's effect already been applied (folded into the set)? */
1417
+ declare function isApplied(applied: AppliedEffects, key: EffectKey): boolean;
1418
+ /**
1419
+ * A keyed external effect. `key` is the durable dedup identity; `fn` is the
1420
+ * irreversible external write (HTTP write, DB upsert, finding/ticket publish).
1421
+ */
1422
+ interface IdempotentEffect<A> {
1423
+ readonly key: EffectKey;
1424
+ readonly fn: () => Promise<A> | A;
1425
+ }
1426
+ /**
1427
+ * Pair a durable `key` with an external effect `fn` — the brick's primary
1428
+ * surface. Run it through {@link appliedEffects}'s guard; a re-fire whose key is
1429
+ * already applied is a proven no-op (`fn` is not invoked a second time).
1430
+ */
1431
+ declare function idempotentEffect<A>(key: EffectKey, fn: () => Promise<A> | A): IdempotentEffect<A>;
1432
+ /**
1433
+ * The outcome of running a keyed effect through the guard. A boolean-discriminated
1434
+ * union so the skipped arm cannot carry a (never-produced) `result`/`event`:
1435
+ *
1436
+ * - `ran: true` — first application: `fn` ran, `result` is its value, and
1437
+ * `event` is the `effect_applied` marker the caller MUST persist (fold into
1438
+ * State) so future re-fires are swallowed.
1439
+ * - `ran: false` — the key was already applied: `fn` was NOT invoked, the
1440
+ * duplicate is swallowed, and there is no new event to persist.
1441
+ */
1442
+ type IdempotentOutcome<A> = {
1443
+ readonly ran: true;
1444
+ readonly key: EffectKey;
1445
+ readonly result: A;
1446
+ readonly event: EffectApplied;
1447
+ } | {
1448
+ readonly ran: false;
1449
+ readonly key: EffectKey;
1450
+ };
1451
+ /**
1452
+ * A live guard over an applied-marker set. Mirrors `pendingEffectsLedger`'s
1453
+ * recorder shape: it keeps an in-memory mirror folded from the same events the
1454
+ * caller persists, so the dedup decision is available without a re-read.
1455
+ *
1456
+ * - `run(effect)` — apply-once: skip (no `fn`) if the key is already applied,
1457
+ * else await `fn` and yield the `effect_applied` event to PERSIST.
1458
+ * - `forget(key)` — prune a marker once its effect can no longer re-fire;
1459
+ * yields the `effect_forgotten` event to PERSIST.
1460
+ * - `applied()` — the current folded set.
1461
+ * - `isApplied(key)`— convenience membership test against the mirror.
1462
+ */
1463
+ interface AppliedEffectsGuard {
1464
+ run<A>(effect: IdempotentEffect<A>): Promise<IdempotentOutcome<A>>;
1465
+ forget(key: EffectKey): {
1466
+ readonly event: EffectForgotten;
1467
+ };
1468
+ applied(): AppliedEffects;
1469
+ isApplied(key: EffectKey): boolean;
1470
+ }
1471
+ /**
1472
+ * Build a live guard. `restore` rehydrates it after an eviction: pass the events
1473
+ * read back from the log (or the applied set carried in the reloaded snapshot,
1474
+ * as single-element `effect_applied` events) so the mirror reflects what was
1475
+ * already applied. Omit `restore` for a fresh actor.
1476
+ */
1477
+ declare function appliedEffects(restore?: {
1478
+ readonly events?: Iterable<AppliedEffectsEvent>;
1479
+ }): AppliedEffectsGuard;
1480
+
1481
+ /**
1482
+ * @demlik/tea/do — generic hibernatable PRESENCE + BROADCAST for a NATIVE
1483
+ * (non-agent) Durable-Object grain.
1484
+ *
1485
+ * ## The gap this fills
1486
+ *
1487
+ * `./host` ships `acceptCommandSocket` / `acceptDurableCommandSocket` /
1488
+ * `broadcast` / `broadcastHibernatable`, but those are framed for — and live
1489
+ * alongside — the LLM-agent **deferred-tool gateway** (`createAgentHost`): the
1490
+ * socket is "the command-runner socket", the frame is "a command", and the
1491
+ * fan-out is the agent's `sendCommand` loop. A plain realtime grain — a game
1492
+ * room, a presence list, a collab document — does not have an agent, a gateway,
1493
+ * or commands. It just wants to accept hibernatable client sockets and fan a
1494
+ * frame out to whoever is connected. Today every such grain (vortex's
1495
+ * `ArenaGrain`, #162) hand-rolls the same `ctx.acceptWebSocket(server)` +
1496
+ * `for (const ws of ctx.getWebSockets()) ws.send(...)` boilerplate (#181).
1497
+ *
1498
+ * This module lifts exactly that, decoupled from the agent host:
1499
+ *
1500
+ * - {@link broadcastFrame} — the fan-out, with the **socket set as the input**
1501
+ * (not a `ctx`), so it is a pure function testable with fake sockets and
1502
+ * reusable over any iterable (`ctx.getWebSockets()`, a filtered subset, a
1503
+ * test array). It serializes once, sends to every OPEN socket, and skips a
1504
+ * closed/errored one — returning a {@link BroadcastReport} count rather than
1505
+ * swallowing the skip silently (errors are data).
1506
+ * - {@link registerHibernatableSocket} — register an already-created server
1507
+ * socket on the DO via the Cloudflare **Hibernation API**
1508
+ * (`ctx.acceptWebSocket`), optionally tagging it and persisting a
1509
+ * per-socket attachment that survives eviction. Pure over the `ctx` +
1510
+ * socket seam, so the registration logic is testable with fakes.
1511
+ * - {@link acceptPresenceSocket} — the full upgrade: build the
1512
+ * `WebSocketPair`, register the server end, and return the `server` socket
1513
+ * plus the 101 upgrade `Response`. The one piece that touches the
1514
+ * runtime-only `WebSocketPair` (so it is exercised by type, not by a unit
1515
+ * test without workerd) — the registration it delegates to *is* unit-tested.
1516
+ * - {@link presenceCount} — the live connection count off `ctx.getWebSockets`,
1517
+ * the trivial roster-size lift a presence grain reaches for.
1518
+ *
1519
+ * It is transport/Msg-agnostic: `frame` is `unknown` (JSON-serialized by
1520
+ * default, or via a caller `serialize`), and nothing here references an agent,
1521
+ * a gateway, a Sub, or a Msg. It is ADDITIVE — `./host`'s agent-coupled
1522
+ * helpers keep working untouched for the agent consumer.
1523
+ */
1524
+ /**
1525
+ * The minimal "send a string frame" surface {@link broadcastFrame} needs. A real
1526
+ * Cloudflare `WebSocket` satisfies it structurally (its `send` accepts a string
1527
+ * and it carries a numeric `readyState`), so `ctx.getWebSockets()` flows in
1528
+ * directly; a test supplies a fake with just `send` (and optionally
1529
+ * `readyState`).
1530
+ *
1531
+ * `readyState` is OPTIONAL: when present and not {@link WS_READY_STATE_OPEN},
1532
+ * the socket is skipped WITHOUT calling `send`; when absent, `send` is attempted
1533
+ * and a throw is caught (a fake that models only liveness via a throwing `send`
1534
+ * needs no `readyState`).
1535
+ */
1536
+ interface PresenceSocket {
1537
+ send(message: string): void;
1538
+ readonly readyState?: number;
1539
+ }
1540
+ /**
1541
+ * The Hibernation slice of `DurableObjectState` a presence grain needs — the
1542
+ * accept that hands the socket to the runtime (surviving eviction) and the
1543
+ * getter that repopulates the live set after a wake. A real `DurableObjectState`
1544
+ * satisfies this structurally; a test supplies a fake (no live Workers runtime
1545
+ * needed to exercise the accept/registry seam).
1546
+ */
1547
+ interface PresenceCtx {
1548
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;
1549
+ getWebSockets(tag?: string): WebSocket[];
1550
+ }
1551
+ /**
1552
+ * The OPEN `readyState` value (`WebSocket.READY_STATE_OPEN` in the Cloudflare
1553
+ * runtime). Named here so {@link broadcastFrame} can skip a non-open socket
1554
+ * without referencing the runtime constant (keeping the fold pure + testable).
1555
+ */
1556
+ declare const WS_READY_STATE_OPEN = 1;
1557
+ /**
1558
+ * What a {@link broadcastFrame} fan-out did, surfaced rather than swallowed:
1559
+ * `sent` is the number of sockets the frame reached; `skipped` is the number
1560
+ * passed over (not OPEN, errored on `send`, or the `except` socket). A caller
1561
+ * can log or assert these — a silent fan-out hides a room going dark.
1562
+ */
1563
+ interface BroadcastReport {
1564
+ readonly sent: number;
1565
+ readonly skipped: number;
1566
+ }
1567
+ /** Optional knobs for {@link broadcastFrame}. */
1568
+ interface BroadcastOptions<S extends PresenceSocket> {
1569
+ /**
1570
+ * A socket to exclude from the fan-out — typically the sender, so a collab /
1571
+ * chat frame is not echoed back to its originator. Compared by identity.
1572
+ */
1573
+ readonly except?: S;
1574
+ /**
1575
+ * How to turn `frame` into the wire string. Defaults to `JSON.stringify`.
1576
+ * Override for a non-JSON wire format (e.g. a pre-encoded string passthrough).
1577
+ */
1578
+ readonly serialize?: (frame: unknown) => string;
1579
+ }
1580
+ /**
1581
+ * Serialize `frame` ONCE and send it to every OPEN socket in `sockets`, skipping
1582
+ * any that is closed, errors on `send`, or is the `except` socket. The socket
1583
+ * set is the INPUT (not a `ctx`), so this is a pure function: pass
1584
+ * `ctx.getWebSockets()` in a live grain, a filtered subset for a targeted
1585
+ * fan-out, or a fake array in a test.
1586
+ *
1587
+ * Skip semantics (a dead socket never crashes the broadcast):
1588
+ * - `readyState` present and not {@link WS_READY_STATE_OPEN} → skipped, `send`
1589
+ * not called;
1590
+ * - `send` throws (a socket closing mid-fan-out) → skipped, the throw is
1591
+ * caught (the close lifecycle reaps it);
1592
+ * - the `except` socket → skipped.
1593
+ *
1594
+ * Returns a {@link BroadcastReport} of `{ sent, skipped }` — the skip is data,
1595
+ * not silence.
1596
+ */
1597
+ declare function broadcastFrame<S extends PresenceSocket>(sockets: Iterable<S>, frame: unknown, options?: BroadcastOptions<S>): BroadcastReport;
1598
+ /** Optional knobs for {@link registerHibernatableSocket} / {@link acceptPresenceSocket}. */
1599
+ interface RegisterOptions<A> {
1600
+ /**
1601
+ * Hibernation tags for the socket — the same strings `ctx.getWebSockets(tag)`
1602
+ * filters on, so a grain can fan out to a subset (e.g. a team, a channel).
1603
+ */
1604
+ readonly tags?: readonly string[];
1605
+ /**
1606
+ * A per-socket value to persist via `server.serializeAttachment` so it
1607
+ * survives DO eviction (e.g. the player/session id the grain re-reads in
1608
+ * `webSocketMessage`/`webSocketClose`). Omitted ⇒ no attachment written.
1609
+ */
1610
+ readonly attachment?: A;
1611
+ }
1612
+ /**
1613
+ * The subset of `WebSocket` {@link registerHibernatableSocket} writes to — the
1614
+ * attachment serializer. A real server `WebSocket` satisfies it; a test supplies
1615
+ * a fake that records the serialized attachment.
1616
+ */
1617
+ interface AttachableSocket {
1618
+ serializeAttachment(value: unknown): void;
1619
+ }
1620
+ /**
1621
+ * Register an already-created `server` socket on the DO via the Cloudflare
1622
+ * **Hibernation API**: `ctx.acceptWebSocket(server, tags)` hands the socket to
1623
+ * the runtime (it survives eviction and re-delivers inbound frames to the DO's
1624
+ * `webSocketMessage` lifecycle method), and — when an `attachment` is supplied —
1625
+ * persists it via `server.serializeAttachment` so it survives the wake.
1626
+ *
1627
+ * Pure over the `ctx` + socket seam (no `WebSocketPair` construction), so the
1628
+ * registration logic — that accept is called with the right tags, that the
1629
+ * attachment is serialized iff present — is unit-testable with fakes.
1630
+ * {@link acceptPresenceSocket} is the thin wrapper that also builds the pair.
1631
+ */
1632
+ declare function registerHibernatableSocket<S extends AttachableSocket, A>(ctx: PresenceCtx, server: S, options?: RegisterOptions<A>): void;
1633
+ /** What {@link acceptPresenceSocket} hands back: the live server end (to send
1634
+ * initial frames on) and the 101 upgrade `Response` (to return from `fetch`). */
1635
+ interface PresenceUpgrade {
1636
+ /** The server end of the pair — send the welcome / initial snapshot on it. */
1637
+ readonly server: WebSocket;
1638
+ /** The 101 Switching-Protocols upgrade response carrying the client end. */
1639
+ readonly response: Response;
1640
+ }
1641
+ /**
1642
+ * Accept a client WebSocket upgrade on a native DO and register it for
1643
+ * hibernation. Builds the `WebSocketPair`, registers the server end via
1644
+ * {@link registerHibernatableSocket} (tags + optional attachment), and returns
1645
+ * the `server` socket plus the 101 upgrade {@link PresenceUpgrade.response}.
1646
+ *
1647
+ * The caller's `fetch` collapses to: send any initial frames on `server`
1648
+ * (welcome, current roster/snapshot), then `return upgrade.response`. This lifts
1649
+ * the `new WebSocketPair()` + `ctx.acceptWebSocket` + `serializeAttachment` +
1650
+ * `new Response(null, { status: 101, webSocket })` boilerplate a presence grain
1651
+ * hand-rolls.
1652
+ *
1653
+ * NOTE: `WebSocketPair` is a runtime-only global (no workerd in unit tests), so
1654
+ * this assembly is exercised by TYPE here; its registration seam is unit-tested
1655
+ * via {@link registerHibernatableSocket}.
1656
+ *
1657
+ * @example
1658
+ * async fetch(request: Request): Promise<Response> {
1659
+ * if (request.headers.get("Upgrade") !== "websocket") {
1660
+ * return new Response("expected a websocket upgrade", { status: 426 });
1661
+ * }
1662
+ * const { server, response } = acceptPresenceSocket(this.ctx, {
1663
+ * attachment: { playerId },
1664
+ * });
1665
+ * server.send(JSON.stringify(welcome));
1666
+ * server.send(JSON.stringify(room.snapshot()));
1667
+ * return response;
1668
+ * }
1669
+ */
1670
+ declare function acceptPresenceSocket<A>(ctx: PresenceCtx, options?: RegisterOptions<A>): PresenceUpgrade;
1671
+ /**
1672
+ * The live connection count for a presence grain — `ctx.getWebSockets(tag)
1673
+ * .length`, repopulated by the runtime after a wake. Pass a `tag` to count only
1674
+ * the sockets registered under it. The trivial roster-size lift a presence /
1675
+ * game grain reaches for (vortex's `connectionCount`).
1676
+ */
1677
+ declare function presenceCount(ctx: PresenceCtx, tag?: string): number;
2
1678
 
3
1679
  /**
4
1680
  * @demlik/tea/do — Durable Object adapter for `@demlik/tea`.
5
1681
  *
6
- * Two exports drive the integration:
1682
+ * The integration is built on ONE transport model and one durability primitive:
7
1683
  *
8
1684
  * 1. `doStore<S>(storage, parse, key?)` — `Store<S>` impl backed by
9
1685
  * `DurableObjectStorage`. JSON-stringifies on save, JSON-parses on load
@@ -15,31 +1691,46 @@ import { Store, SubId, Sub } from '../index.js';
15
1691
  * synchronously"). Shape mismatch is NOT a throw — `parse` returns
16
1692
  * `null` and the substrate boots fresh.
17
1693
  *
18
- * 2. `doSubscribe<M, Ctx>()` — handler registry for the two DO-native sub
19
- * types this package owns:
20
- * - `do_alarm` — schedules a real DO alarm via `state.storage.setAlarm`;
21
- * the cooperating DO's `alarm()` lifecycle method dispatches the sub's
22
- * msg. Cleanup clears the alarm.
23
- * - `do_ws` — registers a WebSocket-id'd listener in a per-DO registry
24
- * that the cooperating DO's `webSocketMessage()` consults. Cleanup
25
- * deregisters.
26
- *
27
- * BOTH `do_alarm` and `do_ws` rely on the host DO cooperating: alarm and
28
- * webSocketMessage are DO lifecycle methods, not JS events the handler can
29
- * subscribe to from the outside. The cooperating DO must:
30
- *
31
- * - Maintain a `Ctx` that exposes `state: DurableObjectState` AND the
32
- * subscriber registries (`wsRegistry`, `alarmRegistry`). The handlers read
33
- * from `ctx`.
34
- * - Forward `alarm()` callbacks into the alarm registry by id, dispatching
35
- * the registered msg.
36
- * - Forward `webSocketMessage(ws, data)` callbacks into the ws registry,
37
- * dispatching `sub.msg(data)` for matching `socketId`s.
38
- *
39
- * `src/test-worker.ts` is the canonical example of this cooperation. See
40
- * `test/README.md` for the convention.
1694
+ * 2. `deferredGateway` + `createAgentHost` (from `./host`) — THE transport for
1695
+ * a DO-hosted agent. A `createAgent().toMachine()` machine owns exactly one
1696
+ * Sub type, `DeadlineSub` (the agent's own retry + watchdog timers); it has
1697
+ * no DO-native Sub variant to compose, and it needs none. The gateway owns
1698
+ * each deferred tool round-trip as a Promise the interpret cell awaits:
1699
+ * - Inbound WebSocket frames are bridged straight into `gateway.settle(...)`
1700
+ * from the DO's `webSocketMessage` lifecycle method — a direct dispatch,
1701
+ * NOT a Sub.
1702
+ * - The per-tool deadline is a `setTimeout` inside the gateway, NOT a DO
1703
+ * alarm.
1704
+ * One model: the transport lives outside the Sub system, bridged into
1705
+ * `dispatch` by the gateway. See `./host`'s module doc for the full rationale
1706
+ * and `.patterns/tea/durable-actors.md` for the host-layer north star.
1707
+ *
1708
+ * (Historical note: this module previously also shipped `do_ws` / `do_alarm`
1709
+ * Subs + a `doSubscribe` registry. Because `toMachine` fixes the agent machine's
1710
+ * Sub type to `DeadlineSub`, those Subs could never union into an agent host's
1711
+ * Sub set — they were structurally unusable by the flagship consumer and had no
1712
+ * callers. They were dropped in favor of the gateway as the single transport.)
41
1713
  */
42
1714
 
1715
+ /**
1716
+ * Options for {@link doStore}. Mirrors the optional-bag shape
1717
+ * `doEventSourcedStore` already uses for its `parse`/`keys`, so the two DO
1718
+ * stores read alike.
1719
+ */
1720
+ interface DoStoreOptions<S> {
1721
+ /** Storage key the snapshot cell lives at. Defaults to `@@state`. */
1722
+ readonly key?: string;
1723
+ /**
1724
+ * The serialize half of the boundary (#182) — the INVERSE of `doStore`'s
1725
+ * `parse`. Maps the live `S` to a JSON-safe carrier before `JSON.stringify`,
1726
+ * so a Model holding a `Map`/`Set`/`Date` (which `JSON.stringify` would
1727
+ * silently flatten) round-trips: `parse` reconstructs the rich `S` from the
1728
+ * carrier on load. Omit for a plain-JSON Model (the identity default — the
1729
+ * `Record`-not-`Map` constraint, unchanged). When present, `parse` MUST
1730
+ * accept whatever `serialize` produces.
1731
+ */
1732
+ readonly serialize?: (state: S) => unknown;
1733
+ }
43
1734
  /**
44
1735
  * `Store<S>` over `DurableObjectStorage`.
45
1736
  *
@@ -62,74 +1753,23 @@ import { Store, SubId, Sub } from '../index.js';
62
1753
  * behavior in `init`/`update`. After this, the boundary parse rejects the
63
1754
  * old shape (returns null) and the DO boots fresh, picking up its new
64
1755
  * schema from the next dispatch onward.
65
- */
66
- declare function doStore<S>(storage: DurableObjectStorage, parse: (raw: unknown) => S | null, key?: string): Store<S>;
67
- /**
68
- * A DO alarm sub: fires `msg` at `firesAt` (epoch ms). The substrate diffs
69
- * subs by `id`; same id across transitions = same alarm. To reschedule, emit
70
- * a DIFFERENT id (e.g., `audit-timeout-v2`).
71
- */
72
- type DoAlarmSub<M> = {
73
- id: SubId;
74
- type: "do_alarm";
75
- firesAt: number;
76
- msg: M;
77
- };
78
- /**
79
- * A DO WebSocket sub: routes incoming messages on `socketId` through
80
- * `sub.msg(data)` to dispatch.
81
- */
82
- type DoWsSub<M> = {
83
- id: SubId;
84
- type: "do_ws";
85
- socketId: string;
86
- msg: (data: string) => M;
87
- };
88
- type DoSub<M> = DoAlarmSub<M> | DoWsSub<M>;
89
- type AssertDoSubIsSub<T extends Sub = DoSub<unknown>> = T;
90
- /**
91
- * Registry of pending alarm dispatches keyed by sub `id`. The cooperating DO's
92
- * `alarm()` lifecycle method drains this and dispatches each msg.
93
- *
94
- * One alarm-queue cell at a time per DO (DO has exactly one alarm slot). We
95
- * still key by `id` so multiple alarm subs scheduled in the same reconcile
96
- * pass don't clobber each other — the earliest `firesAt` wins (DO native
97
- * behavior) and `alarm()` drains everything due by the fire time.
98
- */
99
- type DoAlarmRegistry<M> = Map<string, {
100
- firesAt: number;
101
- msg: M;
102
- }>;
103
- /**
104
- * Registry of WebSocket message subscribers keyed by sub `id`. The cooperating
105
- * DO's `webSocketMessage(ws, data)` consults this and dispatches
106
- * `sub.msg(data)` for entries matching the incoming `socketId`.
107
- */
108
- type DoWsRegistry<M> = Map<string, {
109
- socketId: string;
110
- msg: (data: string) => M;
111
- }>;
112
- /**
113
- * Minimum shape a cooperating DO must put on its `Ctx` for `doSubscribe`
114
- * handlers to wire up. Users may extend with anything else they like.
115
- */
116
- interface DoSubscribeCtx<M> {
117
- state: DurableObjectState;
118
- alarmRegistry: DoAlarmRegistry<M>;
119
- wsRegistry: DoWsRegistry<M>;
120
- }
121
- /**
122
- * Build the `subscribe` handler registry for `do_alarm` and `do_ws` subs.
123
1756
  *
124
- * Returned object shape matches `Machine#subscribe`'s `[K in U['type']]:
125
- * (sub, ctx, dispatch) => () => void` contract for `U = DoSub<M>`.
1757
+ * Serialization sharp edge (#182): `save` JSON-serializes the Model, and
1758
+ * `JSON.stringify` does NOT survive a `Map`/`Set`/`Date` (a `Map` stringifies
1759
+ * to `{}`, silently losing every entry). A Model that wants a `Map` (the
1760
+ * natural "roster keyed by id") therefore supplies a `serialize` hook — the
1761
+ * INVERSE of `parse` — so the boundary owns both directions: `serialize` turns
1762
+ * the live `S` into a JSON-safe carrier on the way out, `parse` reconstructs
1763
+ * the rich `S` from that carrier on the way back in. They are a matched pair:
1764
+ * `parse(JSON.parse(JSON.stringify(serialize(state))))` must round-trip to a
1765
+ * value equal to `state`. Omit `serialize` and the Model must stay plain-JSON
1766
+ * (the `Record`-not-`Map` constraint the default imposes, unchanged).
126
1767
  *
127
- * Generic `Ctx` is user-defined and must extend `DoSubscribeCtx<M>` so handlers
128
- * can reach `state.storage`, `alarmRegistry`, and `wsRegistry`.
1768
+ * @param parse - boundary parse, the deserialize half (`unknown -> S | null`).
1769
+ * @param keyOrOptions - either the storage key (legacy positional form,
1770
+ * defaults to `@@state`) or a {@link DoStoreOptions} bag carrying `key`
1771
+ * and/or the `serialize` hook. A bare string is treated as `{ key }`.
129
1772
  */
130
- declare function doSubscribe<M, Ctx extends DoSubscribeCtx<M>>(): {
131
- do_alarm: (sub: DoAlarmSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
132
- do_ws: (sub: DoWsSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
133
- };
1773
+ declare function doStore<S>(storage: DurableObjectStorage, parse: (raw: unknown) => S | null, keyOrOptions?: string | DoStoreOptions<S>): Store<S>;
134
1774
 
135
- export { type AssertDoSubIsSub, type DoAlarmRegistry, type DoAlarmSub, type DoSub, type DoSubscribeCtx, type DoWsRegistry, type DoWsSub, doStore, doSubscribe };
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 };