experimental-a2 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
  4. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/actor-shared-DFET5q16.js.map +1 -0
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
  17. package/dist/ai-EEfKyc-z.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +4 -2
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +214 -67
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  25. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  26. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +3 -3
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +86 -73
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
  40. package/dist/client-BQS4i_V9.js.map +1 -0
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
  52. package/dist/internal-CvsrprnU.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-CIeL33wj.js +12 -0
  65. package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.js +3 -3
  68. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  69. package/dist/scheduler-vercel.d.ts +2 -2
  70. package/dist/scheduler-vercel.js +3 -3
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
  73. package/dist/server-DtFrWxi3.js.map +1 -0
  74. package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
  75. package/dist/server-qgez0Xny.d.ts.map +1 -0
  76. package/dist/server.browser.js +1 -1
  77. package/dist/server.browser.js.map +1 -1
  78. package/dist/server.d.ts +4 -4
  79. package/dist/server.js +2 -2
  80. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  81. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  82. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  83. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  84. package/dist/store-D0MhBvg6.d.ts +532 -0
  85. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js.map +1 -1
  87. package/dist/store-codec-DwrWlgQx.js +65 -0
  88. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +406 -225
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
  94. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +1598 -587
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  100. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.js +4 -4
  103. package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
  104. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  105. package/dist/store-redis.d.ts +1 -1
  106. package/dist/store-redis.d.ts.map +1 -1
  107. package/dist/store-redis.js +4 -5
  108. package/dist/store-redis.js.map +1 -1
  109. package/dist/store-sqlite.d.ts +1 -1
  110. package/dist/store-sqlite.d.ts.map +1 -1
  111. package/dist/store-sqlite.js +527 -299
  112. package/dist/store-sqlite.js.map +1 -1
  113. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  114. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  115. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  116. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  117. package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
  118. package/dist/wire-CV-Z7wz-.js.map +1 -0
  119. package/docs/01-quickstart.mdx +21 -15
  120. package/docs/02-examples.mdx +1 -1
  121. package/docs/actors/01-introduction.mdx +13 -2
  122. package/docs/actors/02-concurrency.mdx +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +12 -11
  125. package/docs/agents/03-conversations.mdx +36 -25
  126. package/docs/agents/04-harnesses.mdx +14 -7
  127. package/docs/client/03-presence.mdx +18 -14
  128. package/docs/contract/01-events.mdx +2 -2
  129. package/docs/contract/02-state.mdx +18 -14
  130. package/docs/contract/03-sessions.mdx +32 -25
  131. package/docs/index.mdx +29 -24
  132. package/docs/reference/01-core.mdx +7 -6
  133. package/docs/reference/02-server.mdx +250 -191
  134. package/docs/reference/04-ai.mdx +33 -23
  135. package/docs/reference/05-telemetry.mdx +27 -13
  136. package/docs/reference/06-errors.mdx +25 -19
  137. package/docs/reference/meta.ts +1 -1
  138. package/docs/schedulers/01-overview.mdx +8 -8
  139. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  140. package/docs/schedulers/03-qstash.mdx +4 -4
  141. package/docs/server/01-consumers.mdx +418 -0
  142. package/docs/server/02-timers.mdx +31 -21
  143. package/docs/server/03-cancellation.mdx +52 -44
  144. package/docs/server/04-routes.mdx +4 -1
  145. package/docs/server/05-sql-projections.mdx +391 -0
  146. package/docs/server/06-guarantees.mdx +61 -51
  147. package/docs/stores/01-overview.mdx +3 -3
  148. package/docs/stores/02-redis.mdx +24 -10
  149. package/docs/stores/03-redis-http.mdx +8 -6
  150. package/docs/stores/04-postgres.mdx +65 -14
  151. package/docs/stores/05-sqlite.mdx +5 -3
  152. package/docs/upgrades/01-overview.mdx +31 -0
  153. package/docs/upgrades/78-0-22.mdx +115 -0
  154. package/docs/upgrades/79-0-21.mdx +165 -0
  155. package/docs/upgrades/80-0-20.mdx +43 -0
  156. package/docs/upgrades/81-0-19.mdx +33 -0
  157. package/docs/upgrades/82-0-18.mdx +33 -0
  158. package/docs/upgrades/83-0-17.mdx +69 -0
  159. package/docs/upgrades/meta.ts +3 -0
  160. package/examples/playground/DEPLOY.md +39 -7
  161. package/examples/playground/app/a0/chat.test.tsx +2 -2
  162. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  163. package/examples/playground/app/a0/sandbox.ts +58 -13
  164. package/examples/playground/app/a0/server.ts +4 -4
  165. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  166. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  167. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  168. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  169. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  170. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  171. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  172. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  173. package/examples/playground/app/a2slack/db/a2.ts +209 -0
  174. package/examples/playground/app/a2slack/db/index.ts +73 -0
  175. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  176. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  177. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  178. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  179. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  180. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  181. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  182. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  183. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  184. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
  186. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  187. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  188. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  189. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  190. package/examples/playground/app/a2slack/events/route.ts +36 -0
  191. package/examples/playground/app/a2slack/identity.ts +44 -0
  192. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  193. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  194. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  195. package/examples/playground/app/a2slack/lag.ts +73 -0
  196. package/examples/playground/app/a2slack/layout.tsx +43 -0
  197. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  198. package/examples/playground/app/a2slack/model.test.ts +208 -0
  199. package/examples/playground/app/a2slack/model.ts +369 -0
  200. package/examples/playground/app/a2slack/models.test.ts +72 -0
  201. package/examples/playground/app/a2slack/page.tsx +10 -0
  202. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  203. package/examples/playground/app/a2slack/search/route.ts +40 -0
  204. package/examples/playground/app/a2slack/search-index.ts +108 -0
  205. package/examples/playground/app/a2slack/server.test.ts +315 -0
  206. package/examples/playground/app/a2slack/server.ts +55 -0
  207. package/examples/playground/app/a2slack/sessions.ts +50 -0
  208. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  209. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  210. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  211. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  212. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  213. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  214. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  215. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  216. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  217. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  218. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  219. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  220. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  221. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  222. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  223. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  224. package/examples/playground/app/agent/server.ts +3 -3
  225. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  226. package/examples/playground/app/chat/server.ts +45 -35
  227. package/examples/playground/app/components/demo-icons.tsx +7 -0
  228. package/examples/playground/app/components/demos.tsx +7 -0
  229. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  230. package/examples/playground/app/orders/page.tsx +1 -1
  231. package/examples/playground/app/orders/server.ts +49 -30
  232. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  233. package/examples/playground/app/recovery/page.tsx +4 -4
  234. package/examples/playground/app/recovery/server.ts +23 -32
  235. package/examples/playground/drizzle.config.ts +14 -0
  236. package/examples/playground/lib/store.ts +1 -1
  237. package/examples/playground/next.config.ts +6 -1
  238. package/examples/playground/package.json +12 -1
  239. package/examples/playground/scripts/migrate.mjs +31 -0
  240. package/examples/playground/vercel.json +9 -0
  241. package/examples/playground/vitest.drills.config.ts +32 -0
  242. package/package.json +5 -2
  243. package/sql/postgres/drizzle-schema.ts +209 -0
  244. package/sql/postgres/schema.sql +227 -67
  245. package/src/actor-shared.ts +10 -2
  246. package/src/actor.ts +101 -71
  247. package/src/ai-agent.ts +162 -111
  248. package/src/ai-control-server.ts +8 -8
  249. package/src/ai-fx.ts +324 -82
  250. package/src/ai-harness.ts +17 -2
  251. package/src/ai-pi.ts +2 -2
  252. package/src/ai-server.ts +2 -2
  253. package/src/ai.ts +7 -7
  254. package/src/client.ts +19 -2
  255. package/src/contract.ts +1 -1
  256. package/src/deterministic-id.ts +27 -4
  257. package/src/errors.ts +8 -5
  258. package/src/idempotent-replay.ts +0 -1
  259. package/src/index.ts +1 -1
  260. package/src/internal.ts +57 -16
  261. package/src/otel.ts +1 -1
  262. package/src/postgres-schema.ts +98 -57
  263. package/src/presence-recovery.ts +18 -11
  264. package/src/presence-tombstones.ts +52 -0
  265. package/src/push-envelope.ts +20 -0
  266. package/src/runtime-consumer.ts +10 -0
  267. package/src/scheduler-task.ts +2 -2
  268. package/src/scheduler-vercel.ts +1 -1
  269. package/src/server.browser.ts +1 -1
  270. package/src/server.ts +819 -218
  271. package/src/session-socket.ts +1 -1
  272. package/src/store-codec.ts +66 -0
  273. package/src/store-memory.ts +604 -286
  274. package/src/store-postgres.ts +2006 -857
  275. package/src/store-presence-polling.ts +59 -34
  276. package/src/store-redis-core.ts +1961 -1333
  277. package/src/store-redis-notify.ts +76 -26
  278. package/src/store-redis.ts +2 -3
  279. package/src/store-sqlite.ts +915 -516
  280. package/src/store.ts +241 -102
  281. package/src/telemetry.ts +2 -1
  282. package/src/wire.ts +1 -0
  283. package/dist/actor-C7b-1arn.d.ts.map +0 -1
  284. package/dist/actor-shared-t7w8xIKP.js.map +0 -1
  285. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  286. package/dist/ai-mMiWul1T.js.map +0 -1
  287. package/dist/client-BhHLoNKF.js.map +0 -1
  288. package/dist/contract-BYKASlCV.js.map +0 -1
  289. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  290. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  291. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  292. package/dist/internal-CscSY7K_.js.map +0 -1
  293. package/dist/server-DYAATFuT.d.ts.map +0 -1
  294. package/dist/server-qyb5aJJG.js.map +0 -1
  295. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  296. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  297. package/dist/store-CyLUy86v.d.ts +0 -404
  298. package/dist/store-CyLUy86v.d.ts.map +0 -1
  299. package/dist/store-codec-DTG0Ftek.js +0 -10
  300. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  301. package/dist/store-presence-polling-B1ixeqIS.js +0 -94
  302. package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
  303. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  304. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  305. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  306. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
  307. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  308. package/dist/wire-BKxH1vbP.js.map +0 -1
  309. package/docs/server/01-handlers.mdx +0 -227
package/src/store.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * The A2Store interface — the storage contract every store backend
3
3
  * implements. See specs/a2-implementation.md §2–3.
4
4
  *
5
- * This is the whole storage contract: append, read, dispatch claims,
5
+ * This is the whole storage contract: append, read, consumer cursors,
6
6
  * failure markers, snapshots, and the live stream. The scheduler
7
7
  * needs nothing extra — the armed queue message is its own state, and
8
8
  * the store is the only thing it consults.
@@ -21,58 +21,102 @@ export type Event = {
21
21
  createdAt: Date
22
22
  }
23
23
 
24
- /** The handler dispatch whose append first persisted a child event. */
24
+ /** The consumer attempt whose append first persisted a child event. */
25
25
  export type EventCause = {
26
+ consumer: string
27
+ /** The cursor's lane; null for a concurrent consumer's cursor of one event. */
28
+ lane: string | null
26
29
  index: number
27
30
  attempt: number
28
- /** Size of one named handler append, used to reject truncated retries. */
31
+ /** Size of one named consumer append, used to reject truncated retries. */
29
32
  batchSize?: number
30
33
  }
31
34
 
32
- /**
33
- * What the store persists: immutable event history, including its causal edge,
34
- * plus derived dispatch and failure bookkeeping. The bookkeeping is disposable;
35
- * the event and its cause are not.
36
- */
35
+ /** What the store persists: immutable event history and its causal edge. */
37
36
  export type StoredEvent = Event & {
38
- /** Same-session handler dispatch that appended this event; null means root. */
37
+ /** Same-session consumer attempt that appended this event; null means root. */
39
38
  cause: EventCause | null
40
- /** Session-scoped serial execution key resolved when the event is appended. */
39
+ }
40
+
41
+ /**
42
+ * Where a consumer's cursor stands on its head event. A cursor is one record
43
+ * per (session, consumer, lane): a log cursor for a consumer with one string
44
+ * lane, a lane cursor per lane for a lane function, and a cursor per event
45
+ * (lane null) for a concurrent consumer. Its attempt and failure fields
46
+ * describe the current head and reset when the cursor moves past it.
47
+ */
48
+ export type HeadDispatch = {
49
+ consumer: string
41
50
  lane: string | null
42
- /** Adapter clock time recorded by append settlement, completion, or manual skip; null while pending. */
43
- processedAt: Date | null
44
- /** Dispatch attempt that completed this event; null without dispatch or while pending. */
45
- processedByAttempt: number | null
46
- /** Exact ordered child ids atomically returned by the completing attempt. */
47
- returnedEventIds: string[] | null
48
- /** Adapter clock time recorded by the first durable dispatch claim. */
51
+ /** Adapter clock time recorded by the first durable claim of this head. */
49
52
  firstClaimedAt: Date | null
50
- /** Adapter clock time recorded by the most recent durable dispatch claim. */
53
+ /** Adapter clock time recorded by the most recent durable claim of this head. */
51
54
  lastClaimedAt: Date | null
52
- /** Durable dispatch claims, including claims abandoned by hard kills. */
55
+ /** Durable claims of this head, including claims abandoned by hard kills. */
53
56
  attemptCount: number
54
- /** Current dispatch holder; null when the event is not claimed. */
57
+ /** Current holder; null when the head is not claimed. */
55
58
  claimHolder: string | null
56
59
  /**
57
- * Adapter clock expiry for the current dispatch claim, or, with no holder,
58
- * the end of a failed attempt's retry hold.
60
+ * Adapter clock expiry for the current claim, or, with no holder, the end
61
+ * of a failed attempt's retry hold.
59
62
  */
60
63
  claimExpiresAt: Date | null
61
64
  /**
62
- * Failed attempts: caught handler failures, and lapsed claims a later claim
63
- * took over. This alone drives dead-lettering.
65
+ * Failed attempts on this head: caught failures, and lapsed claims a later
66
+ * claim took over. This alone drives dead-lettering.
64
67
  */
65
68
  failureCount: number
66
- /** Adapter clock time recorded by the most recent caught handler failure. */
69
+ /** Adapter clock time recorded by the most recent caught failure. */
67
70
  lastFailedAt: Date | null
68
- /** Dispatch attempt that produced the most recent caught handler failure. */
71
+ /** Attempt that produced the most recent caught failure. */
69
72
  lastFailedAttempt: number | null
70
- /** The last handler failure, stringified. */
73
+ /** The last failure, stringified. */
71
74
  lastError: string | null
72
75
  /** Adapter clock time recorded when dead-lettered; null otherwise. */
73
76
  failedAt: Date | null
74
- /** Latest clear marker, on claimed events of a cleared session. */
75
- clearIndex?: number
77
+ }
78
+
79
+ /** Where one cursor stands: its position, its head, and the head's dispatch state. */
80
+ export type StoredCursor = HeadDispatch & {
81
+ /** The last index this cursor finished; a log cursor counts from the latest clear marker. */
82
+ position: number
83
+ /** The event it runs next; null when it has nothing to run. */
84
+ head: number | null
85
+ }
86
+
87
+ /** A cursor's head event together with its dispatch state. */
88
+ export type HeadEvent = StoredEvent &
89
+ HeadDispatch & {
90
+ /** Latest clear marker, on claimed heads of a cleared session. */
91
+ clearIndex?: number
92
+ }
93
+
94
+ /** Names one cursor head: the consumer, its lane, and the head's index. */
95
+ export type HeadRef = {
96
+ consumer: string
97
+ lane: string | null
98
+ index: number
99
+ }
100
+
101
+ /**
102
+ * A consumer the caller serves. Work for consumers it does not name is
103
+ * neither pending nor claimed, so a removed consumer's cursors wait for its
104
+ * name to return.
105
+ */
106
+ export type StoreConsumer = {
107
+ name: string
108
+ /**
109
+ * Set for a consumer with one string lane. Its cursor keeps one position
110
+ * over the log and runs the next event whose type is in `types` (null:
111
+ * every type). A missing cursor is created by the first append that
112
+ * carries one of those types: at the start of the log with `backfill`,
113
+ * otherwise just before that append.
114
+ */
115
+ log?: {
116
+ lane: string
117
+ types: readonly string[] | null
118
+ backfill: boolean
119
+ }
76
120
  }
77
121
 
78
122
  /**
@@ -106,8 +150,8 @@ export type StoreStateRead = {
106
150
  export type StoreSnapshotWrite = {
107
151
  index: number
108
152
  state: unknown
109
- /** Unfinished trigger events that durably retain this exact checkpoint. */
110
- pinEventIndexes?: readonly number[]
153
+ /** Unfinished cursor heads that durably retain this exact checkpoint. */
154
+ pins?: readonly HeadRef[]
111
155
  }
112
156
 
113
157
  export type StoreStateReadRequest = {
@@ -115,9 +159,15 @@ export type StoreStateReadRequest = {
115
159
  throughIndex?: number
116
160
  snapshotThroughIndex?: number
117
161
  }
118
- /** The result of atomically claiming every currently eligible event. */
162
+
163
+ /** The result of atomically claiming every currently eligible head. */
119
164
  export type StoreClaimAvailableResult =
120
- | { outcome: 'claimed'; events: StoredEvent[] }
165
+ | {
166
+ outcome: 'claimed'
167
+ events: HeadEvent[]
168
+ /** The session's head when the claim was made: how far behind each event is. */
169
+ head?: number
170
+ }
121
171
  | { outcome: 'busy'; retryAt: Date }
122
172
  | { outcome: 'settled' }
123
173
 
@@ -125,48 +175,61 @@ export type StoreClaimAvailableResult =
125
175
  export type CompleteAttemptResult =
126
176
  { outcome: 'completed'; events: StoredEvent[] } | { outcome: 'superseded' }
127
177
 
128
- /** The result of one claim-renewal heartbeat, both lists in append order. */
178
+ /** The result of one claim-renewal heartbeat, both lists in head order. */
129
179
  export type RenewClaimsResult = {
130
180
  /** Listed claims that remain owned by the holder. */
131
- renewed: number[]
132
- /** Listed claims whose event a newer attempt has durably taken. */
133
- superseded: number[]
181
+ renewed: HeadRef[]
182
+ /** Listed claims a newer attempt has durably taken, or whose cursor moved on. */
183
+ superseded: HeadRef[]
134
184
  }
135
185
 
136
- /** The result of atomically recording a caught handler failure. */
186
+ /** The result of atomically recording a caught failure. */
137
187
  export type FailAttemptResult = {
138
188
  outcome: 'failed' | 'dead_lettered' | 'superseded'
139
189
  failureCount: number
140
190
  }
141
191
 
192
+ /** A consumer an appended event starts work for, with its resolved lane. */
193
+ export type EventTarget = {
194
+ consumer: string
195
+ /** The lane a lane function resolved; null runs the event concurrently. */
196
+ lane: string | null
197
+ }
198
+
142
199
  /** Input to `A2Store.append` — already validated by the machine. */
143
200
  export type AppendEvent = {
144
201
  type: string
145
202
  payload: unknown
146
203
  /** Caller-supplied idempotency key; generated when absent. */
147
204
  id?: string
148
- /** Internal causal edge supplied atomically by handler `session.append`. */
205
+ /** Internal causal edge supplied atomically by consumer `session.append`. */
149
206
  cause?: EventCause
150
- /** Session-scoped serial execution key resolved before persistence. */
151
- lane?: string
152
- /** Internal hint: settle this event in the append transaction; no handler is registered. */
153
- settled?: true
207
+ /**
208
+ * Lane-function and concurrent consumers this event starts work for,
209
+ * resolved before persistence. Log-cursor consumers are matched by type
210
+ * from `StoreConsumer.log` instead.
211
+ */
212
+ targets?: readonly EventTarget[]
154
213
  }
155
214
 
156
- /** A handler-returned event with the deterministic id retries require. */
215
+ /** A consumer-returned event with the deterministic id retries require. */
157
216
  export type ReturnedEvent = AppendEvent & { id: string }
158
217
 
159
218
  /** The rows written by an append and the session's pending state. */
160
219
  export type StoreAppendResult = {
161
220
  events: StoredEvent[]
162
- /** Whether the session contains an event without a completion marker. */
221
+ /**
222
+ * Whether a served consumer has a live head in this session: one that is
223
+ * not dead-lettered. An append whose own batch starts work for a served
224
+ * consumer may report true without checking that work is runnable.
225
+ */
163
226
  hasPending: boolean
164
227
  }
165
228
 
166
229
  export type StoreClearOptions = {
167
230
  sessionId: string
168
231
  ifLastIndex?: number
169
- /** The clearing handler's dispatch, fenced like a handler append. */
232
+ /** The clearing consumer's attempt, fenced like a consumer append. */
170
233
  cause?: EventCause
171
234
  }
172
235
 
@@ -182,15 +245,17 @@ export type StoreReclaimResult = { deleted: number; done: boolean }
182
245
 
183
246
  export type StoreRedriveOptions = {
184
247
  sessionId: string
248
+ consumer: string
185
249
  index: number
186
- /** `retry` clears the dead letter; `skip` settles it without a handler. */
250
+ /** `retry` clears the dead letter; `skip` moves the cursor past it. */
187
251
  action: 'retry' | 'skip'
252
+ consumers?: readonly StoreConsumer[]
188
253
  }
189
254
 
190
255
  export type StoreRedriveResult = {
191
- /** Whether the event was dead-lettered and this operation resolved it. */
256
+ /** Whether the head was dead-lettered and this operation resolved it. */
192
257
  redriven: boolean
193
- /** Whether the session contains an event without a completion marker. */
258
+ /** Whether a served consumer has a live head in this session: one that is not dead-lettered. */
194
259
  hasPending: boolean
195
260
  }
196
261
 
@@ -212,6 +277,20 @@ export const SYSTEM_CLOCK: Clock = {
212
277
 
213
278
  export const RANDOM_IDS: IdSource = () => crypto.randomUUID()
214
279
 
280
+ /**
281
+ * Cursor rules every operation shares:
282
+ *
283
+ * - `consumers` names the consumers the caller serves. Omitted, every cursor
284
+ * counts and no log cursor is created or advanced by type.
285
+ * - A cursor's head is the next event it runs: the lowest unfinished event of
286
+ * its lane, the event itself for a concurrent cursor, or for a log cursor
287
+ * the next event after its position whose type it takes. The clear marker
288
+ * is never a head.
289
+ * - A head is eligible when no live claim or retry hold owns it, it is not
290
+ * dead-lettered, and it lies above the latest clear marker.
291
+ * - Moving a cursor past its head resets the head's attempt and failure
292
+ * fields and releases snapshot pins on it.
293
+ */
215
294
  export interface A2Store {
216
295
  /**
217
296
  * Accepts a batch; the batch is atomic — one transaction, consecutive
@@ -221,18 +300,26 @@ export interface A2Store {
221
300
  * lost — return the existing rows as success. If only *some* ids
222
301
  * exist, the caller mixed an already-sent batch with fresh events —
223
302
  * always a caller bug — so throw `A2Error('PARTIAL_DUPLICATE_BATCH')`.
224
- * Events carrying `settled: true` get `processedAt` in this same atomic
225
- * operation, with no dispatch claim or `processedByAttempt`. The result's
226
- * `hasPending` reflects the whole session in the same atomic operation,
227
- * including older events and idempotent retries.
303
+ * Ids are unique per session: the same id in another session names a
304
+ * different event and plays no part in these checks.
305
+ *
306
+ * In the same atomic operation, each event's `targets` start cursor work:
307
+ * a lane target queues the event in that consumer's lane, a null lane
308
+ * creates a cursor for that event alone. Each log consumer in `consumers`
309
+ * whose types the batch carries gets its cursor created or, when idle, a
310
+ * head: the next event after its position that it takes, which may
311
+ * precede the batch when an appender that doesn't serve it wrote in
312
+ * between. `hasPending` reflects the whole session for the served consumers,
313
+ * including older work and idempotent retries.
228
314
  *
229
- * An event carrying `cause` is a handler append, fenced by attempt
230
- * currency: accept it only while `cause.attempt` is still the parent
231
- * event's latest attempt and the parent is not dead-lettered; otherwise
232
- * throw `A2Error('SUPERSEDED_ATTEMPT')` and write nothing. The check is
233
- * part of this atomic operation and must serialize against a concurrent
234
- * `claimAvailable` — an unlocked read of the parent admits write skew.
235
- * The idempotent-replay path runs first, so a batch whose ids all exist
315
+ * An event carrying `cause` is a consumer append, fenced by attempt
316
+ * currency: accept it only while the cause's cursor still has
317
+ * `cause.index` as its head, `cause.attempt` is that head's latest
318
+ * attempt, and the head is not dead-lettered; otherwise throw
319
+ * `A2Error('SUPERSEDED_ATTEMPT')` and write nothing. The check is part of
320
+ * this atomic operation and must serialize against a concurrent
321
+ * `claimAvailable` — an unlocked read of the cursor admits write skew. The
322
+ * idempotent-replay path runs first, so a batch whose ids all exist
236
323
  * replays regardless of the current attempt.
237
324
  *
238
325
  * Clearing stores (a2-implementation.md §17.2) fence one level earlier:
@@ -243,118 +330,162 @@ export interface A2Store {
243
330
  append(
244
331
  sessionId: string,
245
332
  events: AppendEvent[],
246
- options?: { originIndex?: number },
333
+ options?: {
334
+ originIndex?: number
335
+ consumers?: readonly StoreConsumer[]
336
+ },
247
337
  ): Promise<StoreAppendResult>
248
338
 
249
339
  /**
250
340
  * Optional with `reclaim`. Serialized with `append`: checks
251
341
  * `ifLastIndex` against the head and the latest marker, then appends a
252
- * settled clear marker at the next index without touching existing
253
- * rows. Afterwards every event at or below the marker is superseded.
342
+ * clear marker at the next index without touching existing rows.
343
+ * Afterwards every event at or below the marker is superseded: no head
344
+ * lies there, and every log cursor's position counts from the marker.
254
345
  */
255
346
  clear?(options: StoreClearOptions): Promise<StoreClearResult>
256
347
 
257
348
  /**
258
349
  * Deletes one bounded batch of rows below the latest clear marker, with
259
- * their bookkeeping, id records, and cached folds. `done` once nothing
350
+ * their cursor records, id records, and cached folds. `done` once nothing
260
351
  * remains below it. Never waits on a lock that other operations hold.
261
352
  */
262
353
  reclaim?(sessionId: string): Promise<StoreReclaimResult>
263
354
 
264
355
  /**
265
- * Optional with `redrive`. The session's dead-lettered events above the
266
- * latest clear marker, oldest first.
356
+ * Optional with `redrive`. The session's dead-lettered heads above the
357
+ * latest clear marker, in event order, then by consumer name.
267
358
  */
268
- deadLetters?(sessionId: string): Promise<StoredEvent[]>
359
+ deadLetters?(sessionId: string): Promise<HeadEvent[]>
269
360
 
270
361
  /**
271
- * Optional with `deadLetters`. Atomically resolves one dead-lettered event
272
- * above the latest clear marker; any other event is untouched and reports
273
- * `redriven: false`. `retry` clears `failedAt` and `failureCount`, so the
274
- * event is claimable again with a fresh failure budget, keeping its claim
275
- * history and last failure. `skip` records `processedAt` with no
276
- * `processedByAttempt` or `returnedEventIds` and exposes the next event in
277
- * its lane. Both serialize with `claimAvailable` and the handler-append
278
- * parent fence; `skip` also serializes with lane promotion in `append` and
279
- * `completeAttempt`. `hasPending` is read in the same step.
362
+ * Optional with `deadLetters`. Atomically resolves the consumer's
363
+ * dead-lettered head at `index` above the latest clear marker; anything
364
+ * else is untouched and reports `redriven: false`. `retry` clears
365
+ * `failedAt` and `failureCount`, so the head is claimable again with a
366
+ * fresh failure budget, keeping its claim history and last failure. `skip`
367
+ * moves the cursor past the head with no completion. Both serialize with
368
+ * `claimAvailable` and the consumer-append fence. `hasPending` is read in
369
+ * the same step.
280
370
  */
281
371
  redrive?(options: StoreRedriveOptions): Promise<StoreRedriveResult>
282
372
 
373
+ /**
374
+ * Brings one log consumer's cursor up to date without an append, for
375
+ * `session.consume()`. A missing cursor is created: with `backfill`, at
376
+ * the latest clear marker, taking the first event of its types after it;
377
+ * otherwise at the head, with nothing to run. When the session has nothing
378
+ * after its latest clear (or was never written), a missing cursor stays
379
+ * missing: nothing is written, and the cursor comes back idle at the
380
+ * marker, so a consume of an unknown session leaves no trace. That is
381
+ * exact with `backfill`; without it, the consumer starts at the first
382
+ * consume or serving append that finds events. An
383
+ * idle cursor takes the next event of its types after its position, so it
384
+ * finds events appended by deployments that don't serve it. A cursor with
385
+ * a head, claimed or not, is left as it is. Records the consumer's types,
386
+ * as an append would, and returns the cursor and the session's head.
387
+ */
388
+ wakeLog(options: {
389
+ sessionId: string
390
+ consumer: StoreConsumer & { log: NonNullable<StoreConsumer['log']> }
391
+ }): Promise<{ cursor: StoredCursor; head: number }>
392
+
393
+ /**
394
+ * The session's cursors: every log and lane cursor, and every concurrent
395
+ * cursor that still has a head, by consumer, then lane (concurrent cursors
396
+ * last, by head). Lane and concurrent cursors whose work lies at or below
397
+ * the latest clear marker are not listed.
398
+ */
399
+ cursors(sessionId: string): Promise<StoredCursor[]>
400
+
283
401
  /** Events for one session, oldest first. Bounds form `(afterIndex, throughIndex]`. */
284
402
  read(
285
403
  sessionId: string,
286
404
  opts?: { afterIndex?: number; throughIndex?: number },
287
405
  ): Promise<StoredEvent[]>
288
406
 
289
- /** Public history without dispatch metadata; core falls back to read when absent. */
407
+ /** Public history without causal edges; core falls back to read when absent. */
290
408
  readHistory?(
291
409
  sessionId: string,
292
410
  opts?: { afterIndex?: number; throughIndex?: number },
293
411
  ): Promise<Event[]>
294
412
 
295
413
  /**
296
- * Atomically claims every eligible pending event. Unlaned events are all
297
- * independently eligible. Within a lane, only the lowest-index unfinished
298
- * event is eligible. A live claim produces `busy` only when it is the sole
299
- * remaining obstacle to actionable work. Claimed events are returned in event-log
300
- * order. Excluded rows remain lane barriers.
414
+ * Atomically claims every eligible head of the served consumers. A live
415
+ * claim or retry hold produces `busy` only when it is the sole remaining
416
+ * obstacle to actionable work. Claimed heads are returned in event order,
417
+ * then by consumer name. Excluded heads stay unclaimed and keep blocking
418
+ * their lane.
301
419
  */
302
420
  claimAvailable(options: {
303
421
  sessionId: string
304
422
  holder: string
305
423
  ttlMs: number
306
424
  expiresAtMs?: number
307
- excludeIndexes?: readonly number[]
425
+ exclude?: readonly HeadRef[]
426
+ consumers?: readonly StoreConsumer[]
308
427
  /**
309
428
  * Taking over a lapsed claim (one whose holder is still recorded, never
310
429
  * completed or failed) adds one to `failureCount`. When that reaches
311
- * `maxFailures`, the event dead-letters instead of being claimed: it
430
+ * `maxFailures`, the head dead-letters instead of being claimed: it
312
431
  * comes back in `events` with `failedAt` set and no claim. Its
313
432
  * `attemptCount` still advances, fencing the lapsed attempt, and its
314
433
  * `lastFailedAttempt` is unchanged.
315
434
  */
316
435
  maxFailures?: number
436
+ /**
437
+ * Claims only heads at or below this index: a head past it is neither
438
+ * claimed nor reported `busy`, and stays claimable without it.
439
+ */
440
+ through?: number
317
441
  }): Promise<StoreClaimAvailableResult>
318
442
 
319
443
  /**
320
444
  * Renews the listed live claims still owned by `holder`. Each listed claim
321
- * carries the attempt ordinal the holder owns. `renewed` lists the claims
322
- * that remain owned after the operation; `superseded` lists the claims
323
- * whose event's `attemptCount` has durably passed the listed attempt. An
324
- * expired claim no successor has taken appears in neither list — its
445
+ * carries the attempt the holder owns. `renewed` lists the claims that
446
+ * remain owned after the operation; `superseded` lists the claims whose
447
+ * head a newer attempt has durably taken, or whose cursor has moved past
448
+ * it. An expired claim no successor has taken appears in neither list — its
325
449
  * attempt may still complete (`completeAttempt` is the fence), so it is
326
450
  * not reported as lost. Renewal never revives an expired claim.
327
451
  */
328
452
  renewClaims(options: {
329
453
  sessionId: string
330
454
  holder: string
331
- claims: readonly { index: number; attempt: number }[]
455
+ claims: readonly (HeadRef & { attempt: number })[]
332
456
  ttlMs: number
333
457
  expiresAtMs?: number
334
458
  }): Promise<RenewClaimsResult>
335
459
 
336
460
  /**
337
- * Atomically completes one current attempt and appends its returned events.
338
- * Retrying a committed completion with the same attempt and deterministic
339
- * child ids returns the existing children. A stale attempt never appends.
461
+ * Atomically completes one current attempt: appends its returned events
462
+ * (with their own targets, waking the served log consumers) and moves the
463
+ * cursor past its head. Retrying a committed completion with the same
464
+ * attempt and deterministic child ids returns the existing children. A
465
+ * stale attempt never appends.
340
466
  */
341
467
  completeAttempt(options: {
342
468
  sessionId: string
469
+ consumer: string
470
+ lane: string | null
343
471
  index: number
344
472
  attempt: number
345
473
  events: ReturnedEvent[]
474
+ consumers?: readonly StoreConsumer[]
346
475
  }): Promise<CompleteAttemptResult>
347
476
 
348
477
  /**
349
478
  * Atomically records a caught failure for one claimed attempt. A stale
350
- * attempt cannot poison a processed event or a newer dispatch. Accepted
479
+ * attempt cannot poison a finished head or a newer attempt. Accepted
351
480
  * failures record the operation's clock time. A failure that does not
352
- * dead-letter releases the claim and, with `retryAfterMs`, keeps the event
481
+ * dead-letter releases the claim and, with `retryAfterMs`, keeps the head
353
482
  * unclaimable for that long: its claim expiry is set with no holder, so
354
483
  * `claimAvailable` reports it `busy` until then.
355
484
  */
356
485
  failAttempt(options: {
357
486
  sessionId: string
487
+ consumer: string
488
+ lane: string | null
358
489
  index: number
359
490
  attempt: number
360
491
  error: string
@@ -389,9 +520,10 @@ export interface A2Store {
389
520
 
390
521
  /**
391
522
  * Atomically writes disposable reducer checkpoints. The greatest index
392
- * advances the head cache. Older checkpoints survive only when at least one
393
- * listed trigger event is still unfinished; completion and dead-lettering
394
- * release that event's pins and collect unreferenced historical checkpoints.
523
+ * advances the head cache. Older checkpoints survive only while at least
524
+ * one listed cursor head is still unfinished; moving that cursor past it
525
+ * or dead-lettering it releases the pin and collects unreferenced
526
+ * historical checkpoints.
395
527
  */
396
528
  putSnapshots(
397
529
  sessionId: string,
@@ -408,11 +540,15 @@ export interface A2Store {
408
540
  presence?: {
409
541
  /**
410
542
  * Field-wise last-writer-wins merge of one participant's values:
411
- * a field whose existing row has a strictly newer `at` (the
412
- * sender's stamp) is left untouched; `null` deletes the row. Every
413
- * applied write refreshes that field's expiry to the storage's own
543
+ * a field whose unexpired entry has a strictly newer `at` (the
544
+ * sender's stamp) is left untouched. `null` deletes the value but
545
+ * keeps a tombstone with its `at`, which takes part in the merge
546
+ * like a value until it expires, so a set older than the delete
547
+ * still loses when it arrives later. Every applied write, value or
548
+ * tombstone, refreshes that field's expiry to the storage's own
414
549
  * clock plus `ttlMs` — the sender stamp orders writes but never
415
- * anchors their lifetime.
550
+ * anchors their lifetime. `subscribe` delivers applied writes only,
551
+ * a delete as `null`.
416
552
  */
417
553
  set(
418
554
  ns: string,
@@ -421,7 +557,10 @@ export interface A2Store {
421
557
  meta: { seen: number; at: Date; ttlMs: number },
422
558
  ): Promise<void>
423
559
 
424
- /** The current map, pruned of rows at or past their `expiresAt`. */
560
+ /**
561
+ * The current map, pruned of rows at or past their `expiresAt`.
562
+ * Tombstones never appear.
563
+ */
425
564
  read(ns: string): Promise<PresenceRow[]>
426
565
 
427
566
  /**
package/src/telemetry.ts CHANGED
@@ -18,6 +18,7 @@ export type A2SpanName =
18
18
  | 'a2.state'
19
19
  | 'a2.snapshot'
20
20
  | 'a2.redrive'
21
+ | 'a2.consume'
21
22
  | 'a2.ai.control'
22
23
  | 'a2.ai.ack'
23
24
  | 'a2.ai.generate'
@@ -30,7 +31,7 @@ export type A2SpanHandle = {
30
31
  setAttribute(key: string, value: A2AttributeValue): void
31
32
  /**
32
33
  * Mark the span failed without a2's control flow throwing — used for
33
- * handler failures, which a2 swallows by design (they go into the
34
+ * consumer failures, which a2 swallows by design (they go into the
34
35
  * retry machinery, not up the stack) but telemetry must still see.
35
36
  */
36
37
  recordError(error: unknown): void
package/src/wire.ts CHANGED
@@ -314,6 +314,7 @@ const ERROR_STATUS: Record<A2ServerErrorCode, number> = {
314
314
  STORE_NOT_CONFIGURED: 500,
315
315
  UNKNOWN_PRESENCE_FIELD: 400,
316
316
  PRESENCE_NOT_SUPPORTED: 500,
317
+ CONSUMER_DEAD_LETTERED: 500,
317
318
  }
318
319
 
319
320
  export function errorStatus(code: A2ErrorCode): number {
@@ -1 +0,0 @@
1
- {"version":3,"file":"actor-C7b-1arn.d.ts","names":[],"sources":["../src/actor-shared.ts","../src/actor.ts"],"mappings":";;;;;;;;;;;cAiCa,0BAA0B;;WAE5B;;WAEA;EACT,YAAY,eAAe,mBAAmB;;;;;;;;;KAepC;EACV;EACA;;;;;;;EAOA;;;KAIU,gBAAgB,UAAU,iBAAiB;EACrD,gBAAgB;IAEd;;KAIQ,oBAAoB,QAC7B,WAAW,cAAc,EAAE;;;;;;KAQlB,iBAAiB;GAExB,yBAAA,WAAW;IAAgB,OAAO,EAAE;IAAI;IAAc,IAAI;;;;;;;;;;;;KAanD,UAAU,iBACV,WAAW,eAAe,EAAE,MACjC,QAAQ,EAAE,eACV,OAAO,EAAE;;;;;;;;;KAWJ,uBAAuB;EAAmB;;;;;;;;;;;;KAY1C,kBAAkB,iBAClB,WAAW,eAAe,EAAE,MACjC,OAAO,EAAE,gBAAgB,SAAS,yBAAyB,iBAC3D,OAAO,EAAE,IAAI,SAAS,yBAAyB;;KAI1C,aAAa,UAAU;;EAEjC,OAAO;;EAEP;;EAEA;;EAEA,QAAQ;;EAER,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;;;;;;;KASlB,uBAAuB,UAAU;;EAE3C;;EAEA;;EAEA,QAAQ;;EAER,SAAS;IAAU,OAAO;IAAY;;;EAEtC,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;KAGlB,kBAAkB;EAC5B,OAAO;;EAEP;;EAEA;;EAEA;;;KAWU,qBAAqB;EAC/B,kBAAkB,iBAAiB,kBAAkB;;;;;;;;;;;;;;KCzE3C,aAAa,UAAU,eAAe,gBAAgB,iBAC5D,KAAK,aAAa,IAAI,OAAO,YAAY,cAAc;EAEvD;EACA,SACE,KAAK,uBAAuB,IAC5B,OAAO,YAAY,cACT;;;;;;;;;;;;;;;;iBAiBF,WAAW,KAAK,GAC9B,SAAS,KAAK,KAAK,OAAO,aAAa;EACpC;EAAkB,SAAS,KAAK,KAAK,OAAO,aAAa;;KAIlD,aAAa,UAAU;;EAEjC;;EAEA,OAAO;;;;;EAKP,aAAa,WAAW,cAAc,aAAa,GAAG;EACtD,QAAQ;EACR,YAAY;EACZ,YAAY;MACR,gBAAgB;EAChB;;;;;;;;;;;;EAYA;;;;;;;KAQM;WACG;WAAwB;;WAExB;WACA;WACA;;WAGA;WACA;WACA;WACA;WACA;;WAGA;WACA;WACA;WACA,QAAQ,SAAS;;KAGpB,kBAAkB,UAAU;;EAEtC,aAAa,WAAW,6BAA6B;;;;;;;;;;;;EAYrD,QAAQ,OAAO;;KAGL;;EAEV;;KAGU,mBAAmB;;EAE7B;;KAGU,gBAAgB;;EAE1B,OAAO;;EAEP;;;;;EAKA;;KAGG,WACH,UAAU,eACV,gBAAgB,0BACH,YAAY,MAErB,QAAQ,YAAY,IACpB,UAAU,qBACP,QAAQ,gBAAgB,gBAE3B,OAAO,YAAY,IACnB,UAAU,qBACP,QAAQ,gBAAgB;KAE5B,WACH,UAAU,eACV,gBAAgB,0BACH,YAAY,MAErB,QAAQ,YAAY,IACpB,UAAU,qBACP;EAAU;EAAY;MAEzB,OAAO,YAAY,IACnB,UAAU,qBACP;EAAU;EAAY;;;KAGnB,YAAY,UAAU;WACvB;;;;;;;;WAQA,kBAAkB,WAAW,cAAc,WAAW,GAAG;;WAEzD,kBAAkB,WAAW,cAAc,WAAW,GAAG;;WAEzD,aAAa,QAAQ,gBAAgB;;;;;;;;;;;WAWrC,QACP,SAAS,SACT,UAAU,kBAAkB,OACzB,QAAQ;;WAEJ,SAAS,QAAQ;;KAGhB,gBAAgB,UAAU;WAC3B;;;;;;WAMA,WAAW;;WAEX,UAAU,SAAS;;WAEnB,QAAQ,SAAS;;WAEjB,SAAS,QAAQ,qBAAqB,aAAa;EAC5D,MAAM,aAAa,YAAY;;;;;;;iBAsDjB,MAAM,UAAU,eAC9B,SAAS,aAAa,KACrB,gBAAgB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"actor-shared-t7w8xIKP.js","names":["createContract"],"sources":["../src/actor-shared.ts"],"sourcesContent":["import { reducer } from './reducer.ts'\n/**\n * Internal shared vocabulary of the actor module — the reserved event\n * envelopes, the protocol and context types, the state fold. Not an\n * entry point: the public surfaces are experimental-a2/actor (server),\n * /actor/client, and /actor/react.\n *\n * An actor is a durable mailbox with memory: one session per instance,\n * events processed one at a time (a lane) unless a handler opts out\n * with `concurrent: true`, state materialized as `a2.actor.state`\n * events committed atomically with each handler's completion.\n */\n\nimport { internalContract as createContract } from './contract.ts'\nimport type { Reducer } from './reducer.ts'\nimport type { ScheduleTiming } from './server.ts'\nimport type { StandardSchemaV1 } from './standard-schema.ts'\n\n/** Event type of a committed state change — one per completed serial handler. */\nexport const ACTOR_STATE_EVENT = 'a2.actor.state'\n/** Event type of a refusal — a handler that answered by throwing NonRetriableError. */\nexport const ACTOR_FAILED_EVENT = 'a2.actor.failed'\n/** The per-instance lane every serial handler shares — the state's write lock. */\nexport const ACTOR_LANE = 'a2.actor'\n/** Reducer identity — bump when the fold's meaning changes. */\nexport const ACTOR_REDUCER_NAME = 'a2.actor.v1'\n\n/**\n * A handler answered by throwing `NonRetriableError`: the event\n * settled, the state did not change. Isomorphic — the server raises it\n * from calls and `fetch()` serializes it as a 409; the client's `call`\n * proxy revives it from that response.\n */\nexport class ActorRefusedError extends Error {\n /** The refused event's name. */\n readonly event: string\n /** The refused event's message id. */\n readonly messageId: string\n constructor(event: string, messageId: string, message: string) {\n super(message)\n this.name = 'ActorRefusedError'\n this.event = event\n this.messageId = messageId\n }\n}\n\n/**\n * The declaration an actor is defined over: its state shape and its\n * event vocabulary, as types. Core A2 contracts are schemas because\n * the wire is untrusted; actor protocols are types because your\n * server is trusted — same contract-first design language, dialed to\n * the trust level.\n */\nexport type ActorProtocol = {\n state: object\n events: object\n /**\n * Optional third vocabulary: presence field → value type. Ephemeral\n * audience state (who is here, cursors), replicated to subscribers\n * and never stored in the log. Declaring it requires `presence:\n * true` in the actor options — the wire bit types cannot carry.\n */\n presence?: object\n}\n\n/** The protocol's presence vocabulary, `never` when undeclared. */\nexport type ActorPresenceOf<D extends ActorProtocol> = D extends {\n presence: infer P extends object\n}\n ? P\n : never\n\n/** A `setPresence` patch: changed fields, `null` clears one. */\nexport type ActorPresenceValues<P> = {\n [F in keyof P & string]?: P[F] | null\n}\n\n/**\n * The replicated presence map: participant → field → latest value,\n * last write wins per field. Values are peer-authored — render them\n * like user input.\n */\nexport type ActorPresenceMap<P> = {\n [participant: string]: {\n [F in keyof P & string]?: { value: P[F]; seen: number; at: Date }\n }\n}\n\n/**\n * The typed self-send surface: one method per declared event —\n * `ctx.send.transfer({ ref, amount })`. Buffered, not immediate: every\n * send requested during a handler commits atomically with that\n * handler's completion (a serial handler's state commit, a concurrent\n * handler's settlement), riding core's returned-events semantics — so\n * a reserve-and-trigger can never half-happen, and re-runs converge on\n * the same deterministic message ids.\n */\nexport type ActorSend<E> = {\n readonly [K in keyof E]: {} extends E[K]\n ? (input?: E[K]) => void\n : (input: E[K]) => void\n}\n\n/**\n * Timing for a scheduled event: exactly `{ delay: '5d' }` or\n * `{ at: Date }` (core §6 semantics — relative delays anchor to the\n * triggering message's durable `createdAt`, so re-runs resolve the\n * same due time). `name` overrides the timer's identity — it defaults\n * to the target event name, scoped to the triggering message, so one\n * handler run gets one timer per target event unless named apart.\n */\nexport type ActorScheduleOptions = ScheduleTiming & { name?: string }\n\n/**\n * The typed durable-timer surface: one method per declared event —\n * `ctx.schedule.refund({ ref: ctx.id }, { delay: '1h' })`. Unlike\n * `send`, scheduling is immediate, not buffered: it awaits provider\n * acceptance at call time (a timer is provider-side, not a log row),\n * so a handler that schedules and then refuses has still armed the\n * timer. That is safe by the guarded-delivery idiom — the stale timer\n * fires into an idempotent no-op — but it is the one exception to\n * \"refusals are total\". Requires a configured `scheduler`.\n */\nexport type ActorScheduleSend<E> = {\n readonly [K in keyof E]: {} extends E[K]\n ? (input: E[K] | undefined, options: ActorScheduleOptions) => Promise<void>\n : (input: E[K], options: ActorScheduleOptions) => Promise<void>\n}\n\n/** What a serial handler receives alongside its typed input. */\nexport type ActorContext<D extends ActorProtocol> = {\n /** Mutable draft, committed atomically with the handler's completion. */\n state: D['state']\n /** The message id — stable across re-runs; the idempotency key for external I/O. */\n id: string\n /** Durable 1-based dispatch ordinal of this message. */\n attempt: number\n /** The ordinary A2 handler signal — fires on claim expiry or supersession. */\n signal: AbortSignal\n /** Typed buffered self-send — committed atomically with the state commit. */\n send: ActorSend<D['events']>\n /** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */\n schedule: ActorScheduleSend<D['events']>\n}\n\n/**\n * What a `concurrent: true` handler receives. No draft — concurrent\n * handlers run off the lane, in parallel, so state is a snapshot read\n * (honest about staleness) and mutations happen by sending events\n * whose serial handlers decide against fresh state.\n */\nexport type ActorConcurrentContext<D extends ActorProtocol> = {\n /** The message id — stable across re-runs; the idempotency key for external I/O. */\n id: string\n /** Durable 1-based dispatch ordinal of this message. */\n attempt: number\n /** The ordinary A2 handler signal — fires on claim expiry or supersession. */\n signal: AbortSignal\n /** Snapshot read — observational; the world moves while this runs. */\n state(): Promise<{ state: D['state']; index: number }>\n /** Typed buffered self-send — committed atomically with settlement. */\n send: ActorSend<D['events']>\n /** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */\n schedule: ActorScheduleSend<D['events']>\n}\n\nexport type ActorStatePayload<S> = {\n state: S\n /** The event whose handler committed this state. */\n event: string\n /** The message (invocation event) id this state answers. */\n message: string\n /** The session's latest clear marker when this state was read. */\n clearIndex?: number\n}\n\nexport type ActorFailedPayload = {\n /** The message (invocation event) id this refusal answers. */\n message: string\n event: string\n error: string\n}\n\n/** The event vocabulary a client needs to follow an actor's state. */\nexport type ActorClientEventDefs<S> = {\n 'a2.actor.state': StandardSchemaV1<ActorStatePayload<S>>\n}\n\ntype SchemaResult<T> = StandardSchemaV1.Result<T>\n\nconst issue = <T>(message: string): SchemaResult<T> => ({\n issues: [{ message }],\n})\n\nconst schema = <T>(\n label: string,\n parse: (value: unknown) => SchemaResult<T>,\n): StandardSchemaV1<T> => ({\n '~standard': {\n version: 1,\n vendor: 'a2',\n validate(value) {\n try {\n return parse(value)\n } catch (error) {\n return issue(\n `${label}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n },\n },\n})\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** True for plain JSON trees — what the log can store without loss. */\nexport function isJsonTree(\n value: unknown,\n seen: Set<object> = new Set(),\n): boolean {\n if (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'boolean'\n ) {\n return true\n }\n if (typeof value === 'number') return Number.isFinite(value)\n if (typeof value !== 'object') return false\n if (seen.has(value)) return false\n seen.add(value)\n const valid = Array.isArray(value)\n ? value.every((item) => isJsonTree(item, seen))\n : Object.getPrototypeOf(value) === Object.prototype &&\n Object.values(value).every(\n (item) => item === undefined || isJsonTree(item, seen),\n )\n seen.delete(value)\n return valid\n}\n\n/** Per-field ceiling for presence values — presence is a cursor, not a document. */\nexport const PRESENCE_VALUE_MAX_BYTES = 8_192\n\n/**\n * The open presence vocabulary: the '*' catch-all validates any field\n * name against the JSON floor. The protocol types the vocabulary at\n * compile time; the route's `authorize` sees every set before\n * acceptance; renderers treat values as user input. Meaning has three\n * guards — this schema only owns the bytes.\n */\nexport const openPresenceDefs: Readonly<\n Record<string, StandardSchemaV1<unknown>>\n> = Object.freeze({\n '*': {\n '~standard': {\n version: 1,\n vendor: 'a2',\n validate: (value: unknown) => {\n if (!isJsonTree(value)) {\n return {\n issues: [{ message: 'presence values must be plain JSON trees' }],\n }\n }\n if (JSON.stringify(value).length > PRESENCE_VALUE_MAX_BYTES) {\n return {\n issues: [\n {\n message: `presence values are capped at ${PRESENCE_VALUE_MAX_BYTES} bytes`,\n },\n ],\n }\n }\n return { value }\n },\n },\n } satisfies StandardSchemaV1<unknown>,\n})\n\n/**\n * The `a2.actor.state` envelope: provenance plus the state, held to a\n * plain JSON tree (a `Date` or `Map` in state fails loudly at commit,\n * never silently coerces in the log).\n */\nexport function actorStateSchema<S>(): StandardSchemaV1<ActorStatePayload<S>> {\n return schema(ACTOR_STATE_EVENT, (value) => {\n if (\n !isRecord(value) ||\n typeof value['event'] !== 'string' ||\n typeof value['message'] !== 'string'\n ) {\n return issue(`invalid ${ACTOR_STATE_EVENT} payload`)\n }\n const state = value['state']\n if (!isJsonTree(state)) {\n return issue('the actor state must be a plain JSON tree')\n }\n const clearIndex = value['clearIndex']\n if (\n clearIndex !== undefined &&\n (!Number.isSafeInteger(clearIndex) || (clearIndex as number) < 1)\n ) {\n return issue('the actor clearIndex must be a positive safe integer')\n }\n return {\n value: {\n state: state as S,\n event: value['event'],\n message: value['message'],\n ...(clearIndex === undefined\n ? {}\n : { clearIndex: clearIndex as number }),\n },\n }\n })\n}\n\nexport const actorFailedSchema: StandardSchemaV1<ActorFailedPayload> = schema(\n ACTOR_FAILED_EVENT,\n (value) => {\n if (\n !isRecord(value) ||\n typeof value['message'] !== 'string' ||\n typeof value['event'] !== 'string' ||\n typeof value['error'] !== 'string'\n ) {\n return issue(`invalid ${ACTOR_FAILED_EVENT} payload`)\n }\n return {\n value: {\n message: value['message'],\n event: value['event'],\n error: value['error'],\n },\n }\n },\n)\n\nexport type ActorReducerOptions<S> = {\n /** The actor's name — must match the server-side definition. */\n name: string\n /** The initial state — a plain JSON tree, the fold's seed. */\n state: S\n}\n\n/**\n * The library fold over an actor's state commits — last write wins.\n * Pure library code: a browser bundle folds an actor's live state with\n * no user code, so the actor definition itself stays server-only\n * (import the server module's type for typing, this reducer for data).\n */\nexport function actorReducer<S>(\n options: ActorReducerOptions<S>,\n): Reducer<ActorClientEventDefs<S>, S> {\n if (!isJsonTree(options.state)) {\n throw new TypeError(\n `actor '${options.name}': the initial state must be a plain JSON tree`,\n )\n }\n const events: ActorClientEventDefs<S> = {\n [ACTOR_STATE_EVENT]: actorStateSchema(),\n }\n return reducer({\n contract: createContract({ name: options.name, events }),\n name: ACTOR_REDUCER_NAME,\n initialState: options.state,\n }).fold((state, event) =>\n event.type === ACTOR_STATE_EVENT ? event.payload.state : state,\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;AAmBA,MAAa,oBAAoB;;AAEjC,MAAa,qBAAqB;;AAElC,MAAa,aAAa;;AAE1B,MAAa,qBAAqB;;;;;;;AAQlC,IAAa,oBAAb,cAAuC,MAAM;;CAE3C;;CAEA;CACA,YAAY,OAAe,WAAmB,SAAiB;EAC7D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,YAAY;CACnB;AACF;AAkJA,MAAM,SAAY,aAAsC,EACtD,QAAQ,CAAC,EAAE,QAAQ,CAAC,EACtB;AAEA,MAAM,UACJ,OACA,WACyB,EACzB,aAAa;CACX,SAAS;CACT,QAAQ;CACR,SAAS,OAAO;EACd,IAAI;GACF,OAAO,MAAM,KAAK;EACpB,SAAS,OAAO;GACd,OAAO,MACL,GAAG,MAAM,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACpE;EACF;CACF;AACF,EACF;AAEA,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;AAGrE,SAAgB,WACd,OACA,uBAAoB,IAAI,IAAI,GACnB;CACT,IACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,WAEjB,OAAO;CAET,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,SAAS,KAAK;CAC3D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO;CAC5B,KAAK,IAAI,KAAK;CACd,MAAM,QAAQ,MAAM,QAAQ,KAAK,IAC7B,MAAM,OAAO,SAAS,WAAW,MAAM,IAAI,CAAC,IAC5C,OAAO,eAAe,KAAK,MAAM,OAAO,aACxC,OAAO,OAAO,KAAK,CAAC,CAAC,OAClB,SAAS,SAAS,KAAA,KAAa,WAAW,MAAM,IAAI,CACvD;CACJ,KAAK,OAAO,KAAK;CACjB,OAAO;AACT;;AAGA,MAAa,2BAA2B;;;;;;;;AASxC,MAAa,mBAET,OAAO,OAAO,EAChB,KAAK,EACH,aAAa;CACX,SAAS;CACT,QAAQ;CACR,WAAW,UAAmB;EAC5B,IAAI,CAAC,WAAW,KAAK,GACnB,OAAO,EACL,QAAQ,CAAC,EAAE,SAAS,2CAA2C,CAAC,EAClE;EAEF,IAAI,KAAK,UAAU,KAAK,CAAC,CAAC,SAAA,MACxB,OAAO,EACL,QAAQ,CACN,EACE,SAAS,iCAAiC,yBAAyB,QACrE,CACF,EACF;EAEF,OAAO,EAAE,MAAM;CACjB;AACF,EACF,EACF,CAAC;;;;;;AAOD,SAAgB,mBAA8D;CAC5E,OAAO,OAAO,oBAAoB,UAAU;EAC1C,IACE,CAAC,SAAS,KAAK,KACf,OAAO,MAAM,aAAa,YAC1B,OAAO,MAAM,eAAe,UAE5B,OAAO,MAAM,WAAW,kBAAkB,SAAS;EAErD,MAAM,QAAQ,MAAM;EACpB,IAAI,CAAC,WAAW,KAAK,GACnB,OAAO,MAAM,2CAA2C;EAE1D,MAAM,aAAa,MAAM;EACzB,IACE,eAAe,KAAA,MACd,CAAC,OAAO,cAAc,UAAU,KAAM,aAAwB,IAE/D,OAAO,MAAM,sDAAsD;EAErE,OAAO,EACL,OAAO;GACE;GACP,OAAO,MAAM;GACb,SAAS,MAAM;GACf,GAAI,eAAe,KAAA,IACf,CAAC,IACD,EAAc,WAAqB;EACzC,EACF;CACF,CAAC;AACH;AAEA,MAAa,oBAA0D,OACrE,qBACC,UAAU;CACT,IACE,CAAC,SAAS,KAAK,KACf,OAAO,MAAM,eAAe,YAC5B,OAAO,MAAM,aAAa,YAC1B,OAAO,MAAM,aAAa,UAE1B,OAAO,MAAM,WAAW,mBAAmB,SAAS;CAEtD,OAAO,EACL,OAAO;EACL,SAAS,MAAM;EACf,OAAO,MAAM;EACb,OAAO,MAAM;CACf,EACF;AACF,CACF;;;;;;;AAeA,SAAgB,aACd,SACqC;CACrC,IAAI,CAAC,WAAW,QAAQ,KAAK,GAC3B,MAAM,IAAI,UACR,UAAU,QAAQ,KAAK,+CACzB;CAEF,MAAM,SAAkC,GACrC,oBAAoB,iBAAiB,EACxC;CACA,OAAO,QAAQ;EACb,UAAUA,iBAAe;GAAE,MAAM,QAAQ;GAAM;EAAO,CAAC;EACvD,MAAM;EACN,cAAc,QAAQ;CACxB,CAAC,CAAC,CAAC,MAAM,OAAO,UACd,MAAM,SAAA,mBAA6B,MAAM,QAAQ,QAAQ,KAC3D;AACF"}