experimental-a2 0.19.0 → 0.21.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 (320) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
  4. package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
  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-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
  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-CvEnqVfo.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
  17. package/dist/ai-CzE672Q3.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 +1 -1
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +36 -9
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
  25. package/dist/ai-harness-Bfk-Lycn.d.ts.map +1 -0
  26. package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
  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 +4 -4
  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 +84 -75
  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-9IeTsiAs.js} +5 -5
  40. package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
  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-Bc4PvfHk.js} +10 -3
  52. package/dist/internal-Bc4PvfHk.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-BWmht60z.js +12 -0
  65. package/dist/runtime-consumer-BWmht60z.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +7 -7
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  71. package/dist/scheduler-vercel.d.ts +2 -2
  72. package/dist/scheduler-vercel.d.ts.map +1 -1
  73. package/dist/scheduler-vercel.js +5 -4
  74. package/dist/scheduler-vercel.js.map +1 -1
  75. package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
  76. package/dist/server-BshFZ0eL.d.ts.map +1 -0
  77. package/dist/{server-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
  78. package/dist/server-DvIB8kj5.js.map +1 -0
  79. package/dist/server.browser.js +1 -1
  80. package/dist/server.browser.js.map +1 -1
  81. package/dist/server.d.ts +4 -4
  82. package/dist/server.js +2 -2
  83. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
  84. package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
  85. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  86. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  87. package/dist/store-N8PXxDAS.js.map +1 -1
  88. package/dist/store-Z2qjw50n.d.ts +523 -0
  89. package/dist/store-Z2qjw50n.d.ts.map +1 -0
  90. package/dist/store-codec-DwrWlgQx.js +65 -0
  91. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  92. package/dist/store-memory.d.ts +1 -1
  93. package/dist/store-memory.d.ts.map +1 -1
  94. package/dist/store-memory.js +393 -210
  95. package/dist/store-memory.js.map +1 -1
  96. package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
  97. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
  98. package/dist/store-postgres.d.ts +1 -1
  99. package/dist/store-postgres.d.ts.map +1 -1
  100. package/dist/store-postgres.js +1595 -673
  101. package/dist/store-postgres.js.map +1 -1
  102. package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
  103. package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
  104. package/dist/store-redis-http.d.ts +1 -1
  105. package/dist/store-redis-http.js +4 -4
  106. package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
  107. package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
  108. package/dist/store-redis.d.ts +1 -1
  109. package/dist/store-redis.js +2 -2
  110. package/dist/store-sqlite.d.ts +1 -1
  111. package/dist/store-sqlite.d.ts.map +1 -1
  112. package/dist/store-sqlite.js +517 -287
  113. package/dist/store-sqlite.js.map +1 -1
  114. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  115. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  116. package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
  117. package/dist/wire-CZGqNZRx.js.map +1 -0
  118. package/docs/01-quickstart.mdx +28 -19
  119. package/docs/02-examples.mdx +1 -1
  120. package/docs/actors/01-introduction.mdx +9 -1
  121. package/docs/actors/02-concurrency.mdx +1 -1
  122. package/docs/actors/meta.ts +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +13 -12
  125. package/docs/agents/03-conversations.mdx +31 -21
  126. package/docs/agents/04-harnesses.mdx +7 -3
  127. package/docs/agents/meta.ts +1 -1
  128. package/docs/client/03-presence.mdx +18 -14
  129. package/docs/client/meta.ts +1 -1
  130. package/docs/contract/01-events.mdx +2 -2
  131. package/docs/contract/02-state.mdx +18 -14
  132. package/docs/contract/03-sessions.mdx +32 -25
  133. package/docs/index.mdx +31 -26
  134. package/docs/reference/01-core.mdx +12 -12
  135. package/docs/reference/02-server.mdx +247 -192
  136. package/docs/reference/04-ai.mdx +29 -20
  137. package/docs/reference/05-telemetry.mdx +146 -0
  138. package/docs/reference/06-errors.mdx +24 -18
  139. package/docs/reference/meta.ts +1 -1
  140. package/docs/schedulers/01-overview.mdx +112 -0
  141. package/docs/schedulers/02-vercel-queues.mdx +87 -0
  142. package/docs/schedulers/03-qstash.mdx +123 -0
  143. package/docs/schedulers/meta.ts +1 -0
  144. package/docs/server/01-consumers.mdx +415 -0
  145. package/docs/server/02-timers.mdx +33 -23
  146. package/docs/server/03-cancellation.mdx +52 -44
  147. package/docs/server/04-routes.mdx +4 -1
  148. package/docs/server/05-sql-projections.mdx +391 -0
  149. package/docs/server/06-guarantees.mdx +60 -50
  150. package/docs/stores/01-overview.mdx +37 -0
  151. package/docs/stores/02-redis.mdx +57 -0
  152. package/docs/stores/03-redis-http.mdx +56 -0
  153. package/docs/stores/04-postgres.mdx +172 -0
  154. package/docs/stores/05-sqlite.mdx +36 -0
  155. package/docs/stores/06-memory.mdx +22 -0
  156. package/docs/stores/meta.ts +1 -0
  157. package/docs/upgrades/01-overview.mdx +30 -0
  158. package/docs/upgrades/79-0-21.mdx +165 -0
  159. package/docs/upgrades/80-0-20.mdx +43 -0
  160. package/docs/upgrades/81-0-19.mdx +33 -0
  161. package/docs/upgrades/82-0-18.mdx +33 -0
  162. package/docs/upgrades/83-0-17.mdx +69 -0
  163. package/docs/upgrades/meta.ts +3 -0
  164. package/examples/playground/DEPLOY.md +48 -11
  165. package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
  166. package/examples/playground/app/a0/chat.test.tsx +421 -330
  167. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  168. package/examples/playground/app/a0/server.ts +4 -4
  169. package/examples/playground/app/a0/shell.tsx +1 -1
  170. package/examples/playground/app/a0/uploads/route.test.ts +59 -0
  171. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  172. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  173. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  174. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  175. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  176. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  177. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  178. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  179. package/examples/playground/app/a2slack/db/a2.ts +206 -0
  180. package/examples/playground/app/a2slack/db/index.ts +73 -0
  181. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  182. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  183. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  184. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  186. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  187. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  188. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  189. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
  190. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  191. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  192. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  193. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  194. package/examples/playground/app/a2slack/events/route.ts +36 -0
  195. package/examples/playground/app/a2slack/identity.ts +44 -0
  196. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  197. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  198. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  199. package/examples/playground/app/a2slack/lag.ts +73 -0
  200. package/examples/playground/app/a2slack/layout.tsx +43 -0
  201. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  202. package/examples/playground/app/a2slack/model.test.ts +208 -0
  203. package/examples/playground/app/a2slack/model.ts +369 -0
  204. package/examples/playground/app/a2slack/models.test.ts +72 -0
  205. package/examples/playground/app/a2slack/page.tsx +10 -0
  206. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  207. package/examples/playground/app/a2slack/search/route.ts +40 -0
  208. package/examples/playground/app/a2slack/search-index.ts +108 -0
  209. package/examples/playground/app/a2slack/server.test.ts +315 -0
  210. package/examples/playground/app/a2slack/server.ts +55 -0
  211. package/examples/playground/app/a2slack/sessions.ts +50 -0
  212. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  213. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  214. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  215. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  216. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  217. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  218. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  219. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  220. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  221. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  222. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  223. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  224. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  225. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  226. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  227. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  228. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
  229. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
  230. package/examples/playground/app/agent/ingress.test.ts +41 -17
  231. package/examples/playground/app/agent/model-input.test.tsx +92 -43
  232. package/examples/playground/app/agent/server.ts +3 -3
  233. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
  234. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  235. package/examples/playground/app/chat/server.test.ts +38 -65
  236. package/examples/playground/app/chat/server.ts +45 -35
  237. package/examples/playground/app/components/activity-history.test.tsx +60 -18
  238. package/examples/playground/app/components/demo-icons.tsx +7 -0
  239. package/examples/playground/app/components/demos.tsx +7 -0
  240. package/examples/playground/app/components/fast-link.test.tsx +3 -5
  241. package/examples/playground/app/initial-state.test.tsx +86 -45
  242. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  243. package/examples/playground/app/orders/page.tsx +1 -1
  244. package/examples/playground/app/orders/server.ts +49 -30
  245. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  246. package/examples/playground/app/recovery/page.tsx +4 -4
  247. package/examples/playground/app/recovery/server.ts +23 -32
  248. package/examples/playground/drizzle.config.ts +14 -0
  249. package/examples/playground/next.config.ts +6 -1
  250. package/examples/playground/package.json +13 -1
  251. package/examples/playground/scripts/migrate.mjs +31 -0
  252. package/examples/playground/test/agent.ts +35 -0
  253. package/examples/playground/test/model.ts +149 -0
  254. package/examples/playground/test/serve.ts +184 -0
  255. package/examples/playground/test/server-only.ts +1 -0
  256. package/examples/playground/vercel.json +9 -0
  257. package/examples/playground/vitest.config.ts +27 -0
  258. package/examples/playground/vitest.drills.config.ts +32 -0
  259. package/package.json +7 -2
  260. package/sql/postgres/drizzle-schema.ts +206 -0
  261. package/sql/postgres/notifications.sql +24 -0
  262. package/sql/postgres/schema.sql +276 -0
  263. package/src/actor.ts +101 -71
  264. package/src/ai-agent.ts +154 -115
  265. package/src/ai-control-server.ts +8 -8
  266. package/src/ai-fx.ts +41 -9
  267. package/src/ai-harness.ts +28 -2
  268. package/src/ai-pi.ts +2 -2
  269. package/src/ai-server.ts +2 -2
  270. package/src/ai.ts +7 -7
  271. package/src/contract.ts +1 -1
  272. package/src/deterministic-id.ts +26 -3
  273. package/src/errors.ts +9 -6
  274. package/src/idempotent-replay.ts +0 -1
  275. package/src/index.ts +1 -1
  276. package/src/internal.ts +17 -6
  277. package/src/otel.ts +1 -1
  278. package/src/postgres-notifications.ts +0 -22
  279. package/src/postgres-resources.ts +6 -9
  280. package/src/postgres-schema.ts +121 -0
  281. package/src/push-envelope.ts +20 -0
  282. package/src/runtime-consumer.ts +10 -0
  283. package/src/scheduler-qstash.ts +12 -5
  284. package/src/scheduler-task.ts +2 -2
  285. package/src/scheduler-vercel.ts +10 -4
  286. package/src/server.browser.ts +1 -1
  287. package/src/server.ts +839 -238
  288. package/src/session-socket.ts +1 -1
  289. package/src/store-codec.ts +66 -0
  290. package/src/store-memory.ts +581 -255
  291. package/src/store-postgres.ts +1984 -943
  292. package/src/store-redis-core.ts +1624 -1152
  293. package/src/store-redis-notify.ts +2 -0
  294. package/src/store-sqlite.ts +891 -486
  295. package/src/store.ts +227 -97
  296. package/src/telemetry.ts +2 -1
  297. package/src/wire.ts +1 -0
  298. package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
  299. package/dist/ai-harness-CV6rXbB3.d.ts.map +0 -1
  300. package/dist/ai-mMiWul1T.js.map +0 -1
  301. package/dist/contract-BYKASlCV.js.map +0 -1
  302. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  303. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  304. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  305. package/dist/internal-CscSY7K_.js.map +0 -1
  306. package/dist/server-D-Y4pqwu.js.map +0 -1
  307. package/dist/server-DYAATFuT.d.ts.map +0 -1
  308. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  309. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  310. package/dist/store-CyLUy86v.d.ts +0 -404
  311. package/dist/store-CyLUy86v.d.ts.map +0 -1
  312. package/dist/store-codec-DTG0Ftek.js +0 -10
  313. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  314. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  315. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  316. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  317. package/dist/wire-BKxH1vbP.js.map +0 -1
  318. package/docs/reference/05-adapters.mdx +0 -351
  319. package/docs/server/01-handlers.mdx +0 -200
  320. package/docs/server/05-production.mdx +0 -323
@@ -5,61 +5,51 @@ description: "`experimental-a2/server`: `createServer`, its route, the store and
5
5
 
6
6
  ## `experimental-a2/server`
7
7
 
8
- ### `handler(entry)`
9
-
10
- Normalizes a handler entry so you can compose an existing handler without
11
- branching on its shape. For ordinary use, define handlers inline in
12
- `createServer({ handlers })`.
13
-
14
- - Accepts a function or `{ handler, lane?, abortOn? }` and returns the object
15
- shape: a function becomes `{ handler: entry }`, an object is returned
16
- unchanged, and both keep their original types.
17
- - Does not bind a contract; `createServer({ handlers })` checks the match.
18
- Annotate a new handler's context, as below, or pass an already typed
19
- function.
20
- - Spreading the result keeps `lane` and `abortOn`; set either after the spread
21
- to override it. Changing a built-in AI handler's lane can separate it from
22
- the handlers it coordinates with.
23
- - A composition runs in one handler attempt. Work after the original resolves
24
- still runs before its returned events commit, a throw fails the composed
25
- attempt, and a retry can run both again.
26
-
27
- ```ts server/composed-orders.ts
28
- import * as a2 from 'experimental-a2'
29
- import { createServer, handler, type HandlerContext } from 'experimental-a2/server'
30
- import { z } from 'zod'
31
-
32
- const orders = a2.contract({
33
- name: 'composed-orders',
34
- events: {
35
- created: z.object({ shopId: z.string() }),
36
- notified: z.object({ shopId: z.string() }),
37
- },
38
- })
8
+ ### `consumer(options)`
39
9
 
40
- const created = handler({
41
- lane: 'orders',
42
- handler: async (ctx: HandlerContext<typeof orders.events, 'created'>) => {
43
- return orders.batch({ type: 'notified', payload: ctx.event.payload })
44
- },
45
- })
46
-
47
- export const ordersServer = createServer({
48
- contract: orders,
49
- handlers: {
50
- created: {
51
- ...created,
52
- handler: async (ctx) => {
53
- const result = await created.handler(ctx)
54
- // Your additional work goes here:
55
- // await notifyShop(ctx.event.payload, { idempotencyKey: ctx.event.id })
56
- return result
57
- },
58
- },
59
- },
60
- })
10
+ ```ts
11
+ consumer(options: {
12
+ contract: Contract // the contract whose events it reads
13
+ name: string // its identity, unique per server
14
+ types?: readonly EventType[] // default: every type
15
+ lane?: string | ((ctx: LaneContext) => string | null) // default: concurrent
16
+ backfill?: boolean // default false; needs a string lane, and no abortOn
17
+ abortOn?: AbortSpec
18
+ }): { run(fn: (ctx: Context) => Promise<void | AppendInput | readonly AppendInput[]>): Consumer }
19
+ ```
20
+
21
+ Defines a named reader of every session's log. `.run()` takes the function and
22
+ returns the `Consumer` you pass to `createServer({ consumers })` or
23
+ `session.consume()`. In `consumers`, the same options without `contract`, and
24
+ `run`, define one inline on the server's contract. See
25
+ [Consumers](/server/consumers).
26
+
27
+ | Option | Behavior |
28
+ | --- | --- |
29
+ | `name` | 1-128 letters, digits, and `. _ : -`, starting with a letter or digit. Cursors, failures, dead letters, and child event ids key on it, so a new name is a new consumer. Names starting with `a2.` are reserved. |
30
+ | `types` | The event types it runs on, at least one; `ctx.event` narrows to them. Other events start no work for it. Leave it out to take every type. |
31
+ | `lane` | Omitted: every event runs concurrently on its own cursor. A string: one cursor runs every event in log order. A function of `{ sessionId, event }`: the event's lane, or `null` to run it concurrently. Resolved and stored at append. |
32
+ | `backfill` | `true` starts a new consumer at the beginning of the log instead of at the first event appended after it exists. Needs a string `lane` and no `abortOn`; anything else throws a `TypeError`. |
33
+ | `abortOn` | Events that fire `ctx.signal` while `run` works: an array matches by type; an object maps types to predicates `(event, trigger, { attempt })`, whose `attempt` is the trigger's durable attempt, so a cancellation can fence one recovered run. See [Cancellation](/server/cancellation). Not with `backfill`: cancellation is for work in progress, and checking it while replaying history would read the rest of the log for every event. |
34
+
35
+ ```ts
36
+ // consumer options: a cancel that names one recovered run
37
+ abortOn: {
38
+ cancelled: (event, trigger, { attempt }) =>
39
+ event.payload.of === `${trigger.id}:${attempt}`,
40
+ },
61
41
  ```
62
42
 
43
+ - `run` receives the context below, with `ctx.attempt` counting claims of this
44
+ event for this consumer. Returned events commit with the cursor's advance;
45
+ `ctx.session.append` commits immediately. Both derive ids of events without
46
+ an `id` from the trigger and the consumer's name.
47
+ - An invalid name, an empty or unknown type list, or `backfill` with a non-string lane or with `abortOn` throws
48
+ a `TypeError` before any I/O. `createServer` throws on two consumers with
49
+ one name or a consumer of another contract.
50
+ - `Consumer`, `ConsumerOptions`, `ConsumerLane`, `ConsumerResult`, and
51
+ `AbortSpec` are exported from `experimental-a2/server`.
52
+
63
53
  ### `createServer(options)`
64
54
 
65
55
  ```ts
@@ -69,65 +59,42 @@ createServer(options: {
69
59
  scheduler?: A2Scheduler
70
60
  telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
71
61
  presence?: { ttlMs?: number } // presence expiry policy; default 60s
72
- handlers?: {
73
- [type]:
74
- | Handler
75
- | {
76
- abortOn?: AbortSpec
77
- lane?: string | ((ctx: LaneContext) => string)
78
- handler: Handler
79
- }
80
- }
62
+ consumers?: readonly (Consumer | InlineConsumer)[] // see consumer(options)
81
63
  }): A2Server
82
-
83
- type Handler = (
84
- ctx: Context,
85
- ) => Promise<void | AppendInput | readonly AppendInput[]>
86
64
  ```
87
65
 
88
66
  Implements a contract: binds its vocabulary to storage and reactions.
89
67
  `experimental-a2/server` is the only entry point that can reach a store
90
68
  backend; under the browser condition its exports resolve to a loud error. See
91
- [Handlers](/server/handlers).
69
+ [Consumers](/server/consumers). The options type is `ServerOptions`, exported
70
+ from `experimental-a2/server`.
92
71
 
93
72
  | Option | Behavior |
94
73
  | --- | --- |
95
- | `handlers` | One table, complete at construction. Compose files by spreading objects in; a duplicate key silently last-wins. |
96
- | `lane` | Within one session, same-lane events run one at a time in log order; all others run concurrently. A resolver receives `{ sessionId, event }`. The value is resolved and stored at append, so a later deployment can't reinterpret pending work. See [Lanes](/server/handlers#lanes). |
97
- | `abortOn` | Events that fire `ctx.signal` while the handler runs: an array matches by type; an object maps types to predicates `(event, trigger, { attempt })`, whose `attempt` is the trigger's durable attempt, so a cancellation can fence one recovered run. See [Cancellation](/server/cancellation). |
74
+ | `consumers` | The consumers this server runs, each with its own cursors in every session: a `Consumer` from `consumer()`, or `{ name, types?, lane?, backfill?, abortOn?, run }` inline, whose `run` narrows to its `types`. Names must be unique, and every consumer must read this server's contract; otherwise construction throws a `TypeError`. Omit it for a history-only server. |
98
75
  | `presence.ttlMs` | How long a presence value survives without a refreshing set: a positive integer of milliseconds, default 60 seconds, counted on the storage's clock, never the sender's stamp. Only for contracts that declare `presence`; elsewhere construction throws a `TypeError`. |
99
76
 
100
- ```ts
101
- generate: {
102
- abortOn: {
103
- cancelled: (event, trigger, { attempt }) =>
104
- event.payload.of === `${trigger.id}:${attempt}`,
105
- },
106
- handler: async (ctx) => { /* ... */ },
107
- }
108
- ```
109
-
110
- - Handlers are concurrent by default. A2 starts every eligible event once its
111
- append commits.
112
- - A handler returns nothing, one event, or an array. A2 marks the trigger
113
- processed and appends the returned batch in one atomic store operation; when
114
- the handler throws, the returned events never exist.
115
- - Handlers are optional per event type. An event type without one settles in
116
- its append transaction with no dispatch attempt, and starts no drain or
117
- scheduler arm when the session has no older pending handler work. See
118
- [Events without handlers](/server/handlers#events-without-handlers).
77
+ - A2 starts every eligible event once its append commits. A consumer without
78
+ a lane runs its events concurrently.
79
+ - `run` returns nothing, one event, or an array. A2 moves the consumer's
80
+ cursor past the trigger and appends the returned batch in one atomic store
81
+ operation; when `run` throws, the returned events never exist.
82
+ - An event no consumer takes starts no work: no dispatch attempt, and no drain
83
+ or scheduler arm when the session has no older pending work. See
84
+ [Which events it takes](/server/consumers#which-events-it-takes).
119
85
 
120
- The context every handler receives:
86
+ The context every consumer's `run` receives, `ConsumerContext`:
121
87
 
122
88
  | Property | Meaning |
123
89
  | --- | --- |
124
90
  | `ctx.event` | `Event`: the triggering event |
125
- | `ctx.attempt` | durable 1-based dispatch claim ordinal; increments on every durable claim and may skip when a process dies before handler entry |
126
- | `ctx.session` | this session: `id` (equal to `ctx.event.sessionId`), `append`, `schedule`, `clear`, `history`, `state`, `stream`, `setPresence` |
91
+ | `ctx.attempt` | durable 1-based claim ordinal of this event for this consumer; increments on every durable claim and may skip when a process dies before `run` |
92
+ | `ctx.session` | this session: `id` (equal to `ctx.event.sessionId`), `append`, `schedule`, `clear`, `history`, `state`, `stream`, `consume`, `setPresence` |
127
93
  | `ctx.signal` | `AbortSignal`: fires on lease lapse (`CLAIM_EXPIRED`), proven supersession (`SUPERSEDED_ATTEMPT`), and matching `abortOn` events |
128
94
 
129
- `ctx.session` shares the `server.session(id)` surface; handler-specific
130
- behavior is noted under each method. Its `state(reducer)` folds through the
95
+ `ConsumerContext`, `ConsumerAppend`, and `LaneContext` are exported from
96
+ `experimental-a2/server`. `ctx.session` shares the `server.session(id)`
97
+ surface; consumer-specific behavior is noted under each method. Its `state(reducer)` folds through the
131
98
  trigger's index by default (see
132
99
  [`session.state`](#sessionstatereducer-options)), and its `append` takes a
133
100
  name:
@@ -136,12 +103,12 @@ name:
136
103
  ctx.session.append(name: string, ...events: AppendInput[]): Promise<Event[]>
137
104
  ```
138
105
 
139
- - It commits immediately, so its events may run while the handler is still
140
- active unless a lane orders them. See
141
- [Append what should happen now](/server/handlers#append-what-should-happen-now).
106
+ - It commits immediately, so its events may run while `run` is still active
107
+ unless a lane orders them. See
108
+ [Append what should happen now](/server/consumers#append-what-should-happen-now).
142
109
  - The name is the idempotency key, scoped to the trigger, for events whose
143
- `id` is omitted: each such id derives from the trigger id, the name, and the
144
- event's position. The same name and generated-id batch returns the existing
110
+ `id` is omitted: each such id derives from the trigger id, the consumer's
111
+ name, the append's name, and the event's position. The same name and generated-id batch returns the existing
145
112
  rows on retry; a changed batch conflicts with them.
146
113
  - An explicit `id` wins, so different triggering events can converge on one
147
114
  fact. Root `server.session(id).append(...events)` takes no name.
@@ -214,7 +181,7 @@ automatically.
214
181
  - Authentication stays in the route, where headers, cookies, and framework
215
182
  context are available. Capture the authenticated principal in `authorize`.
216
183
  - `authorize` is an ingress policy for this `fetch` call, not a global server
217
- policy: direct `session.append`, handler appends, scheduled appends, and
184
+ policy: direct `session.append`, consumer appends, scheduled appends, and
218
185
  server-side `setPresence` bypass it.
219
186
  - By default the `sessionId` and `index` query parameters bind one session to
220
187
  the socket. A2 authorizes the stream before upgrading, and frames can't
@@ -243,7 +210,7 @@ you append, read, or stream. `session.id` is the `id` passed in. See
243
210
 
244
211
  It has every [session method](#session), plus
245
212
  [`deadLetters()`](#sessiondeadletters) and
246
- [`redrive()`](#sessionredriveindex-options), which `ctx.session` doesn't.
213
+ [`redrive()`](#sessionredrivetarget-options), which `ctx.session` doesn't.
247
214
 
248
215
  ### `server.drain(sessionId)`
249
216
 
@@ -251,43 +218,48 @@ It has every [session method](#session), plus
251
218
  server.drain(sessionId: string): Promise<{ settled: boolean }>
252
219
  ```
253
220
 
254
- Claims every eligible event and runs their handlers concurrently.
221
+ Claims every eligible event for each consumer and runs them concurrently.
255
222
  `settled` means no actionable or live-claimed work remains. Past the
256
223
  invocation's deadline it claims nothing and returns `settled: false`. A dead-lettered
257
224
  event can block later work in its lane while other lanes continue, until
258
- [`session.redrive()`](#sessionredriveindex-options) resolves it. Scheduler
225
+ [`session.redrive()`](#sessionredrivetarget-options) resolves it. Scheduler
259
226
  callbacks use it; you'll rarely call it yourself. The public result stays this
260
227
  simple boolean.
261
228
 
262
229
  ### `A2Store`
263
230
 
264
- Custom store adapters implement these atomic drain methods:
231
+ Custom store adapters keep the log and one cursor per (session, consumer,
232
+ lane). These are the dispatch methods:
265
233
 
266
234
  ```ts
267
235
  // Custom server store adapter:
268
236
  type EventCause = {
237
+ consumer: string
238
+ lane: string | null // null: a concurrent consumer's cursor of one event
269
239
  index: number
270
240
  attempt: number
271
- batchSize?: number // present on named handler appends
241
+ batchSize?: number // present on named consumer appends
272
242
  }
273
243
 
244
+ type EventTarget = { consumer: string; lane: string | null }
245
+
274
246
  type AppendEvent = {
275
247
  type: string
276
248
  payload: unknown
277
249
  id?: string
278
250
  cause?: EventCause
279
- lane?: string
280
- settled?: true // internal: no handler is registered for this event type
251
+ targets?: readonly EventTarget[] // lane-function and concurrent consumers
281
252
  }
282
253
 
283
254
  type ReturnedEvent = AppendEvent & { id: string }
284
255
 
285
- type StoredEvent = Event & {
286
- cause: EventCause | null
256
+ type StoredEvent = Event & { cause: EventCause | null }
257
+
258
+ type HeadRef = { consumer: string; lane: string | null; index: number }
259
+
260
+ type HeadDispatch = {
261
+ consumer: string
287
262
  lane: string | null
288
- processedAt: Date | null
289
- processedByAttempt: number | null
290
- returnedEventIds: string[] | null
291
263
  firstClaimedAt: Date | null
292
264
  lastClaimedAt: Date | null
293
265
  attemptCount: number
@@ -298,16 +270,25 @@ type StoredEvent = Event & {
298
270
  lastFailedAttempt: number | null
299
271
  lastError: string | null
300
272
  failedAt: Date | null
301
- clearIndex?: number // on claimed events, once the session has been cleared
302
273
  }
303
274
 
304
- type StoreAppendResult = {
305
- events: StoredEvent[]
306
- hasPending: boolean
275
+ type HeadEvent = StoredEvent &
276
+ HeadDispatch & {
277
+ clearIndex?: number // on claimed heads, once the session has been cleared
278
+ }
279
+
280
+ type StoredCursor = HeadDispatch & { position: number; head: number | null }
281
+
282
+ type StoreConsumer = {
283
+ name: string
284
+ // a consumer with one string lane: its cursor reads the log by type
285
+ log?: { lane: string; types: readonly string[] | null; backfill: boolean }
307
286
  }
308
287
 
288
+ type StoreAppendResult = { events: StoredEvent[]; hasPending: boolean }
289
+
309
290
  type StoreClaimAvailableResult =
310
- | { outcome: 'claimed'; events: StoredEvent[] }
291
+ | { outcome: 'claimed'; events: HeadEvent[]; head?: number } // head: the session's head when the claim was made
311
292
  | { outcome: 'busy'; retryAt: Date }
312
293
  | { outcome: 'settled' }
313
294
 
@@ -319,9 +300,19 @@ interface A2Store {
319
300
  append(
320
301
  sessionId: string,
321
302
  events: AppendEvent[],
322
- options?: { originIndex?: number },
303
+ options?: { originIndex?: number; consumers?: readonly StoreConsumer[] },
323
304
  ): Promise<StoreAppendResult>
324
305
 
306
+ cursors(sessionId: string): Promise<StoredCursor[]>
307
+
308
+ // for session.consume(): create a missing log cursor (at the clear marker
309
+ // with backfill, else at the head), give an idle one its next event,
310
+ // leave one with a head alone; returns it and the session's head
311
+ wakeLog(options: {
312
+ sessionId: string
313
+ consumer: StoreConsumer & { log: NonNullable<StoreConsumer['log']> }
314
+ }): Promise<{ cursor: StoredCursor; head: number }>
315
+
325
316
  // optional together; required for session.clear()
326
317
  clear?(options: {
327
318
  sessionId: string
@@ -331,11 +322,13 @@ interface A2Store {
331
322
  reclaim?(sessionId: string): Promise<{ deleted: number; done: boolean }>
332
323
 
333
324
  // optional together; required for session.deadLetters() and redrive()
334
- deadLetters?(sessionId: string): Promise<StoredEvent[]>
325
+ deadLetters?(sessionId: string): Promise<HeadEvent[]>
335
326
  redrive?(options: {
336
327
  sessionId: string
328
+ consumer: string
337
329
  index: number
338
330
  action: 'retry' | 'skip'
331
+ consumers?: readonly StoreConsumer[]
339
332
  }): Promise<{ redriven: boolean; hasPending: boolean }>
340
333
 
341
334
  claimAvailable(options: {
@@ -343,27 +336,33 @@ interface A2Store {
343
336
  holder: string
344
337
  ttlMs: number
345
338
  expiresAtMs?: number
346
- excludeIndexes?: readonly number[]
339
+ exclude?: readonly HeadRef[]
340
+ consumers?: readonly StoreConsumer[]
347
341
  maxFailures?: number
348
342
  }): Promise<StoreClaimAvailableResult>
349
343
 
350
344
  renewClaims(options: {
351
345
  sessionId: string
352
346
  holder: string
353
- claims: readonly { index: number; attempt: number }[]
347
+ claims: readonly (HeadRef & { attempt: number })[]
354
348
  ttlMs: number
355
349
  expiresAtMs?: number
356
- }): Promise<{ renewed: number[]; superseded: number[] }>
350
+ }): Promise<{ renewed: HeadRef[]; superseded: HeadRef[] }>
357
351
 
358
352
  completeAttempt(options: {
359
353
  sessionId: string
354
+ consumer: string
355
+ lane: string | null
360
356
  index: number
361
357
  attempt: number
362
358
  events: ReturnedEvent[]
359
+ consumers?: readonly StoreConsumer[]
363
360
  }): Promise<CompleteAttemptResult>
364
361
 
365
362
  failAttempt(options: {
366
363
  sessionId: string
364
+ consumer: string
365
+ lane: string | null
367
366
  index: number
368
367
  attempt: number
369
368
  error: string
@@ -376,17 +375,27 @@ interface A2Store {
376
375
  }
377
376
  ```
378
377
 
378
+ A cursor's **head** is the next event it runs: the lowest unfinished event of
379
+ its lane, the event itself for a concurrent cursor, or for a log cursor the
380
+ next event after its position whose type it takes. The clear marker is never a
381
+ head. A head is eligible when no live claim or retry hold owns it, it is not
382
+ dead-lettered, and it lies above the latest clear marker. Moving a cursor past
383
+ its head resets the head's attempt and failure fields and releases its
384
+ snapshot pins. `consumers` names the consumers the caller serves: work for any
385
+ other consumer is neither pending nor claimed. Omitted, every cursor counts.
386
+
379
387
  | Method | Atomic effect |
380
388
  | --- | --- |
381
- | `append` | Write a consecutive batch and report session-wide pending handler work as `hasPending`. Events with the internal `settled` flag get `processedAt` in the same transaction, with no dispatch attempt. An event with a `cause` commits only while `cause.attempt` is the parent's latest attempt and the parent isn't dead-lettered; otherwise throw `SUPERSEDED_ATTEMPT`. The check serializes against a concurrent claim, after the idempotent-replay path. |
382
- | `claimAvailable` | Claim every eligible event. All unlaned events are independent; only the lowest-index unfinished event in each lane is eligible. A claim records its holder, expiry, timestamps, and next `attemptCount`. Taking over a lapsed claim, one its holder never completed or failed, adds one to `failureCount`; when that reaches `maxFailures`, dead-letter the event instead of claiming it: advance `attemptCount` to fence the lapsed attempt, leave `lastFailedAttempt` unchanged, and return it with `failedAt` set. A failure's retry hold has no holder, so it is no lapsed claim. |
383
- | `renewClaims` | Extend the listed live claims the holder still owns. `renewed` lists those; `superseded` lists claims whose event's `attemptCount` passed the listed attempt. An expired claim no successor took is in neither and may still complete; renewal never revives it. |
384
- | `completeAttempt` | In one transaction: fence on the current attempt, mark the parent processed, store its exact ordered `returnedEventIds`, and append the returned batch. A same-attempt retry returns the committed children; a stale attempt returns `superseded`. |
385
- | `failAttempt` | Record one current caught failure, clear its claim, and dead-letter at `maxFailures`. Short of that, with `retryAfterMs` the event stays unclaimable for that long: set its claim expiry with no holder, so `claimAvailable` reports it `busy` until then. A repeated acknowledgment is idempotent; a stale attempt returns `superseded`. |
386
- | `clear` | Serialized with `append`. Check `ifLastIndex` against the head and the latest marker, then append a settled clear marker at the next index, touching no existing rows. A `cause` is fenced like a handler append. |
387
- | `reclaim` | Delete one bounded batch of rows below the latest clear marker, with their bookkeeping, event-id records, lane pointers, and cached state. Report how many rows went, and `done` when none are left. Never wait on a lock other methods hold. |
388
- | `deadLetters` | Return the unprocessed events with `failedAt` set above the latest clear marker, oldest first. |
389
- | `redrive` | Only for an unprocessed event with `failedAt` set above the latest clear marker. `retry` clears `failedAt` and `failureCount`, leaving claims and the last failure. `skip` sets `processedAt` with no `processedByAttempt` or `returnedEventIds` and exposes the next event in its lane. Anything else reports `redriven: false` and writes nothing. Both serialize with claims and handler appends, `skip` also with lane promotion; report `hasPending` like `append`. |
389
+ | `append` | Write a consecutive batch. Each event's `targets` start work: a lane target queues the event in that consumer's lane cursor, a null lane creates a cursor for that event alone. Each log consumer in `consumers` whose types the batch carries gets its cursor created (at the start of the log with `backfill`, otherwise just before the batch) or, when idle, a head. Report pending work for the served consumers as `hasPending`. An event with a `cause` commits only while that cursor's head is `cause.index`, `cause.attempt` is its latest attempt, and it isn't dead-lettered; otherwise throw `SUPERSEDED_ATTEMPT`. The check serializes against a concurrent claim, after the idempotent-replay path. |
390
+ | `cursors` | List every log and lane cursor, and every concurrent cursor that still has a head, by consumer, then lane, with concurrent cursors last by head. Lane and concurrent cursors whose work lies at or below the latest clear marker are not listed. |
391
+ | `claimAvailable` | Claim every eligible head of the served consumers, in event order, then by consumer. A claim records its holder, expiry, timestamps, and next `attemptCount`. Taking over a lapsed claim, one its holder never completed or failed, adds one to `failureCount`; when that reaches `maxFailures`, dead-letter the head instead of claiming it: advance `attemptCount` to fence the lapsed attempt, leave `lastFailedAttempt` unchanged, and return it with `failedAt` set. Excluded heads stay unclaimed and keep blocking their lane. A claim may report `head`, the session's head when the claim was made. |
392
+ | `renewClaims` | Extend the listed live claims the holder still owns. `renewed` lists those; `superseded` lists claims whose head a newer attempt took, or whose cursor moved past it. An expired claim no successor took is in neither and may still complete; renewal never revives it. |
393
+ | `completeAttempt` | In one transaction: fence on the current attempt, append the returned batch with its own targets, record the completion with its exact ordered child ids, and move the cursor past its head. A same-attempt retry returns the committed children; a stale attempt returns `superseded`. |
394
+ | `failAttempt` | Record one current caught failure on the cursor, clear its claim, and dead-letter at `maxFailures`. Short of that, with `retryAfterMs` the head stays unclaimable for that long: set its claim expiry with no holder, so `claimAvailable` reports it `busy` until then. A repeated acknowledgment is idempotent; a stale attempt returns `superseded`. |
395
+ | `clear` | Serialized with `append`. Check `ifLastIndex` against the head and the latest marker, then append a clear marker at the next index. Log cursors count from the marker; lane and concurrent cursors at or below it are superseded. A `cause` is fenced like a consumer append. |
396
+ | `reclaim` | Delete one bounded batch of rows below the latest clear marker, with their cursor records, event-id records, and cached state. Report how many rows went, and `done` when none are left. Never wait on a lock other methods hold. |
397
+ | `deadLetters` | Return the dead-lettered heads above the latest clear marker, in event order, then by consumer. |
398
+ | `redrive` | Only for the consumer's dead-lettered head at `index` above the latest clear marker. `retry` clears `failedAt` and `failureCount`, leaving claims and the last failure. `skip` moves the cursor past it with no completion. Anything else reports `redriven: false` and writes nothing. Both serialize with claims and consumer appends; report `hasPending` like `append`. |
390
399
 
391
400
  `readHistory(sessionId, options)` is an optional capability with the same
392
401
  exclusive `afterIndex` and inclusive `throughIndex` bounds as `read()`. Without
@@ -396,14 +405,14 @@ A store that supports clearing treats every event at or below the latest
396
405
  marker as superseded, whether or not its row still exists. Each check belongs
397
406
  inside the operation's existing atomic step:
398
407
 
399
- - `claimAvailable` never claims it; lane readiness at append and completion
400
- ignores it, so a dead-lettered lane head stops blocking.
408
+ - `claimAvailable` never claims it, so a dead-lettered lane head stops
409
+ blocking; a new event in that lane becomes its head.
401
410
  - `renewClaims` reports it in `superseded`; `completeAttempt` and
402
411
  `failAttempt` return `superseded`.
403
412
  - An `append` whose `cause` names it throws `SUPERSEDED_ATTEMPT`; one whose
404
413
  `originIndex` is at or below the marker throws `SESSION_CLEARED`.
405
414
  - Reads start at the marker; `readState` returns its index as `clearIndex`,
406
- and claimed events carry it as `clearIndex`.
415
+ and claimed heads carry it as `clearIndex`.
407
416
  - In `append` and `completeAttempt`, an event id whose row is below this
408
417
  session's marker is free to reuse.
409
418
 
@@ -414,15 +423,18 @@ stores support both.
414
423
 
415
424
  Stored fields:
416
425
 
417
- - `attemptCount` counts claims, including abandoned ones. `failureCount`
418
- counts failed attempts: caught failures, and lapsed claims a later claim
419
- took over. Ten dead-letter the event. See
426
+ - `attemptCount` counts claims of the current head, including abandoned ones.
427
+ `failureCount` counts its failed attempts: caught failures, and lapsed
428
+ claims a later claim took over. Ten dead-letter the head. Both start over
429
+ when the cursor moves on. See
420
430
  [Guarantees](/server/guarantees#one-event-many-attempts).
421
- - `cause` names the event and handler attempt whose `ctx.session.append` or
422
- return value first persisted the child; `null` means a top-level event.
423
- - `lane` is the session-scoped serialized group, resolved before append.
424
- - `returnedEventIds` makes a lost completion acknowledgment recoverable without
425
- accepting a partial child batch.
431
+ - `cause` names the consumer, lane, event, and attempt whose
432
+ `ctx.session.append` or return value first persisted the child; `null`
433
+ means a top-level event.
434
+ - `lane` is the cursor's lane, resolved before append; `null` is a concurrent
435
+ cursor of one event.
436
+ - A completion's recorded child ids make a lost acknowledgment recoverable
437
+ without accepting a partial child batch.
426
438
  - Lifecycle timestamps are adapter clock values for their atomic operations,
427
439
  not exact database commit times. Built-in adapters persist all these fields
428
440
  inside their existing atomic operations, with no extra round trip.
@@ -438,7 +450,7 @@ Custom scheduler adapters implement two methods:
438
450
 
439
451
  Application routes call `scheduler.handler(...servers)` on the configured
440
452
  adapter. See
441
- [Production](/server/production#2-add-a-scheduler-for-handlers-and-timers).
453
+ [Schedulers](/schedulers/overview).
442
454
 
443
455
  `SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask |
444
456
  SchedulerClearTask`. These task types, plus `ScheduledEvent`, are exported
@@ -453,7 +465,7 @@ from `experimental-a2/server`.
453
465
 
454
466
  Drain tasks wake pending work and carry no application payload. Append tasks
455
467
  carry the events for `session.schedule`; `originIndex` is the scheduling
456
- handler's event, absent for root schedules. Clear tasks carry the condition for
468
+ consumer's trigger, absent for root schedules. Clear tasks carry the condition for
457
469
  `session.schedule.clear`.
458
470
 
459
471
  Deliver an authenticated, normalized append or clear task through the server
@@ -482,7 +494,7 @@ verifies that the task and server contract match.
482
494
 
483
495
  - `deliverSchedulerAppend` revalidates the carried event inputs and uses the
484
496
  ordinary top-level append. The target server must have a scheduler. When
485
- the append leaves pending handler work, it resolves only after a recovery
497
+ the append leaves pending consumer work, it resolves only after a recovery
486
498
  watchdog is durable, so a send failure keeps the delivery retryable. A task
487
499
  whose `originIndex` was cleared resolves without appending.
488
500
  - `deliverSchedulerClear` runs `clear({ ifLastIndex })` and resolves after its
@@ -507,9 +519,9 @@ The only way to move a session forward. See [Guarantees](/server/guarantees).
507
519
  - An explicit `id` makes it idempotent across retries; re-sending an identical
508
520
  batch returns the original rows.
509
521
  - Events pushed through `server.fetch` arrive through this same append.
510
- - Only event types with registered handlers are dispatched; the append itself
511
- settles the rest. An unhandled append still starts session healing when
512
- older handled work is pending. The store reports that pending state in the
522
+ - Only events some consumer takes are dispatched; the append itself settles
523
+ the rest. An append no consumer takes still starts session healing when
524
+ older work is pending. The store reports that pending state in the
513
525
  atomic append, so the decision needs no follow-up read.
514
526
 
515
527
  ### `session.append.dispatch(...events)`
@@ -522,10 +534,10 @@ session.append.dispatch(
522
534
 
523
535
  Commits the same atomic batch, then sends pending work to the configured
524
536
  scheduler instead of starting an inline drain. See
525
- [Dispatch in a fresh invocation](/server/production#dispatch-in-a-fresh-invocation).
537
+ [Dispatch in a fresh invocation](/schedulers/overview#dispatch-in-a-fresh-invocation).
526
538
 
527
539
  - Awaits acceptance of an immediate scheduler message. A batch that leaves no
528
- pending handler work sends none.
540
+ pending consumer work sends none.
529
541
  - Throws before writing when the server has no scheduler.
530
542
  - If the send fails, the events are already durable: give them explicit IDs
531
543
  and retry the same dispatch.
@@ -546,7 +558,7 @@ session.schedule(
546
558
  ```
547
559
 
548
560
  Appends the events later through the configured scheduler, from root and
549
- handler sessions. Resolves after provider acceptance, not after append. `name`
561
+ consumer sessions. Resolves after provider acceptance, not after append. `name`
550
562
  must be non-empty, with at least one event. `ScheduleDelay`, `ScheduleTiming`,
551
563
  and `SessionSchedule` are exported from `experimental-a2/server`. See
552
564
  [Timers](/server/timers) for stale-timer guards, provider limits, and
@@ -554,7 +566,7 @@ deployment compatibility.
554
566
 
555
567
  | Timing | Meaning |
556
568
  | --- | --- |
557
- | `{ delay: '30s' }` | relative delay with unit `ms`, `s`, `m`, `h`, or `d`; from the call's wall clock on a root session, from the trigger's durable `createdAt` in a handler, so handler retries keep the delivery time |
569
+ | `{ delay: '30s' }` | relative delay with unit `ms`, `s`, `m`, `h`, or `d`; from the call's wall clock on a root session, from the trigger's durable `createdAt` in a consumer, so retries keep the delivery time |
558
570
  | `{ at: Date }` | absolute delivery time; use it when a retried root call needs one target |
559
571
 
560
572
  - The template type checks the unit. At runtime the number must be an
@@ -563,9 +575,10 @@ deployment compatibility.
563
575
  once are rejected.
564
576
  - The target is the earliest useful delivery time. Adapters encode it in
565
577
  whole-second slots; an already-due target is sent immediately.
566
- - The task id derives from the contract, session, and name. Handler tasks add
567
- the trigger id and, once the session has been cleared, the clear, so a
568
- trigger id reused in a new lifetime schedules a new task. Omitted event ids
578
+ - The task id derives from the contract, session, and name. A consumer's
579
+ tasks add the trigger id, the consumer's name, and, once the session has
580
+ been cleared, the clear, so a trigger id reused in a new lifetime schedules
581
+ a new task. Omitted event ids
569
582
  derive from the task id and position; explicit ids win.
570
583
  - Reuse a name only for identical timing and events. Provider deduplication is
571
584
  an optimization; stable event ids make later duplicate deliveries converge in
@@ -575,11 +588,11 @@ deployment compatibility.
575
588
  functions, symbols, cycles, accessors, and non-plain prototypes are rejected;
576
589
  shared references become independent values.
577
590
  - Delivery validates the snapshotted schema input through an ordinary
578
- top-level append, outside any handler's completion transaction, so a schema
579
- transform produces the stored payload from the original input. The events
580
- have `cause: null` and run through handlers, lanes, state, and streams
581
- normally.
582
- - A handler task belongs to its trigger's lifetime: if the session is cleared
591
+ top-level append, outside any consumer's completion transaction, so a
592
+ schema transform produces the stored payload from the original input. The
593
+ events have `cause: null` and run through consumers, lanes, state, and
594
+ streams normally.
595
+ - A consumer's task belongs to its trigger's lifetime: if the session is cleared
583
596
  before delivery, it is acknowledged without appending. A root task joins
584
597
  whichever lifetime exists at delivery, and its name identifies one task
585
598
  across lifetimes, so put something from the new lifetime in the name when
@@ -611,10 +624,10 @@ and `index` is always the head after the call:
611
624
  | otherwise | `{ cleared: true, index }`, the new marker |
612
625
 
613
626
  - Every event at or below the marker is superseded: never dispatched again,
614
- never blocking its lane. Its running handler's appends throw
627
+ never blocking its lane. A run still working on one has its appends throw
615
628
  `SUPERSEDED_ATTEMPT` and its state reads throw `SESSION_CLEARED` rather than
616
629
  fold the new lifetime. Exception: after a clear from another instance, a
617
- read bounded at the handler's own event may return that event's state from
630
+ read bounded at the run's own event may return that event's state from
618
631
  memory until its `ctx.signal` aborts.
619
632
  - Pushes and appends after the marker start the new lifetime.
620
633
  - `clear()` then deletes the old events, their bookkeeping and event-id
@@ -624,8 +637,8 @@ and `index` is always the head after the call:
624
637
  - If deletion fails, the call rejects with the session already cleared; any
625
638
  later clear of the session, including a retry, finishes the deletion.
626
639
  - Old event ids can be used again. Presence is not cleared.
627
- - On `ctx.session`, the clear is fenced like the handler's appends and ends the
628
- handler's own attempt: `ctx.signal` aborts with `SUPERSEDED_ATTEMPT` and
640
+ - On `ctx.session`, the clear is fenced like the run's appends and ends the
641
+ run's own attempt: `ctx.signal` aborts with `SUPERSEDED_ATTEMPT` and
629
642
  returned events are discarded. Its deletion keeps running under the
630
643
  platform's `waitUntil`.
631
644
  - All built-in stores support clearing.
@@ -647,19 +660,19 @@ works as in `session.schedule`, with the same provider limits. See
647
660
  [Scheduling a clear](/contract/sessions#scheduling-a-clear).
648
661
 
649
662
  - The task id derives from the contract, session, name, and condition, plus
650
- the trigger id and any earlier clear inside a handler. Reuse a name only for
651
- the same timing.
663
+ the trigger id, the consumer's name, and any earlier clear inside a
664
+ consumer. Reuse a name only for the same timing.
652
665
  - Delivery runs `clear({ ifLastIndex })` and acknowledges after the deletion
653
666
  finishes. No event is appended; a stale or duplicate delivery changes
654
667
  nothing. Indexes never repeat, so a condition from an older lifetime never
655
668
  matches a newer one.
656
669
  - `ifLastIndex: ctx.event.index` matches only while the trigger is still the
657
- head. The handler's own appends and returned events move the head, so a
658
- handler that writes should use the index of its last `ctx.session.append`.
670
+ head. The consumer's own appends and returned events move the head, so a
671
+ consumer that writes should use the index of its last `ctx.session.append`.
659
672
  Actor handlers always commit state, so they can't use the trigger's index.
660
- - A delay short enough to deliver while the handler still runs can match
661
- before its writes land; the clear then ends the handler's attempt and
662
- discards its returned events.
673
+ - A delay short enough to deliver while `run` still works can match before its
674
+ writes land; the clear then ends that attempt and discards its returned
675
+ events.
663
676
  - Each delivery attempt resumes the deletion. Vercel Queues redelivers until
664
677
  retention ends. QStash retries up to its `retries` budget; a deletion that
665
678
  outlasts them leaves the message in QStash's dead-letter queue, the rest of
@@ -672,6 +685,7 @@ works as in `session.schedule`, with the same provider limits. See
672
685
  session.deadLetters(): Promise<DeadLetter[]>
673
686
 
674
687
  type DeadLetter = Event & {
688
+ consumer: string
675
689
  lane: string | null
676
690
  attemptCount: number
677
691
  failureCount: number
@@ -680,35 +694,37 @@ type DeadLetter = Event & {
680
694
  }
681
695
  ```
682
696
 
683
- The session's dead-lettered events, oldest first, each with why it failed. See
684
- [When an event dead-letters](/server/production#3-when-an-event-dead-letters).
697
+ The session's dead letters in event order, then by consumer, each with why it
698
+ failed. One event can be dead for one consumer and done for another. See
699
+ [Resolve a dead letter](/server/consumers#resolve-a-dead-letter).
685
700
 
686
701
  - `failureCount` counts the attempts that spent its budget: caught failures,
687
702
  and attempts that ended before they finished. `attemptCount` counts every
688
- claim. `lastError` is the last error a handler threw, stringified, or null
703
+ claim. `lastError` is the last error `run` threw, stringified, or null
689
704
  when every failed attempt ended before it finished.
690
705
  - Events at or below the latest clear marker are never listed.
691
706
  - Only on `server.session(id)`. `DeadLetter` is exported from
692
707
  `experimental-a2/server`.
693
708
 
694
- ### `session.redrive(index, options)`
709
+ ### `session.redrive(target, options)`
695
710
 
696
711
  ```ts
697
712
  session.redrive(
698
- index: number,
713
+ target: { consumer: string; index: number },
699
714
  options: { action: 'retry' | 'skip' },
700
715
  ): Promise<{ redriven: boolean }>
701
716
  ```
702
717
 
703
- Resolves the dead-lettered event at `index` in one atomic store operation.
718
+ Resolves the consumer's dead-lettered event at `index` in one atomic store
719
+ operation. Every dead letter names its consumer, A2's own included.
704
720
 
705
721
  | Action | Effect |
706
722
  | --- | --- |
707
- | `retry` | The handler runs again as a new attempt, with a fresh ten-failure budget. Once it completes, its lane continues. |
708
- | `skip` | The event is marked processed without running its handler, and the next event in its lane runs. Its effects never happen. |
723
+ | `retry` | The consumer runs the event again as a new attempt, with a fresh ten-failure budget. Once it completes, its lane continues. |
724
+ | `skip` | The consumer's cursor moves past the event without running it, and the next event in its lane runs. Its effects never happen for that consumer. |
709
725
 
710
- - `index` is a positive safe integer and `action` is required; anything else
711
- throws a `TypeError` before any I/O.
726
+ - `consumer` is a consumer's name, `index` is a positive safe integer, and
727
+ `action` is required; anything else throws a `TypeError` before any I/O.
712
728
  - `redriven` is `false`, with nothing written, when the event isn't
713
729
  dead-lettered: still pending, already processed, already redriven, missing,
714
730
  or at or below the latest clear marker. Calling it again is safe.
@@ -717,12 +733,50 @@ Resolves the dead-lettered event at `index` in one atomic store operation.
717
733
  `schedulerArmTimeoutMs`. A failed send doesn't reject the call; it marks the
718
734
  `a2.redrive` span and leaves later appends, `server.drain()`, or another
719
735
  `redrive` to wake the session.
720
- - A retried event keeps its `attemptCount` and last failure, so the next run's
721
- `ctx.attempt` keeps counting. A skipped event keeps `failedAt` and has no
722
- `processedByAttempt`.
736
+ - A retried head keeps its `attemptCount` and last failure, so the next run's
737
+ `ctx.attempt` keeps counting. A skip moves the consumer's cursor on, and its
738
+ next head starts with fresh counts.
723
739
  - Only on `server.session(id)`. `SessionRedriveOptions` and
724
740
  `SessionRedriveResult` are exported from `experimental-a2/server`.
725
741
 
742
+ ### `session.consume(consumer, options?)`
743
+
744
+ ```ts
745
+ session.consume(
746
+ consumer: Consumer,
747
+ options?: { through?: number; signal?: AbortSignal },
748
+ ): Promise<{ index: number }>
749
+ ```
750
+
751
+ Runs a consumer with a string `lane` on this session, here, until it has
752
+ handled every event it takes through `through`: by default the session's head
753
+ when the call starts, and never more than it. Returns the index it has handled
754
+ every event through, at least `through`. See
755
+ [Run a consumer now](/server/consumers#run-a-consumer-now-with-consume).
756
+
757
+ - The consumer's cursor is created on the first call when it's missing: at the
758
+ start of the log with `backfill`, otherwise at the head. An idle cursor also
759
+ finds events appended by deployments that don't run the consumer. A session
760
+ with nothing after its latest clear, or never written, gets no cursor: the
761
+ call writes nothing.
762
+ - The server's own consumer shares its cursor with background drains: a head
763
+ another invocation holds, or one waiting out a failure's backoff, is waited
764
+ for. Nothing past `through` runs here; it carries on in the background. A different
765
+ definition under one of the server's consumer names throws a `TypeError`.
766
+ - A consumer the server doesn't run is claimed only by `consume`, and keeps
767
+ its place between calls.
768
+ - A dead-lettered head throws `CONSUMER_DEAD_LETTERED`. `signal` stops the
769
+ wait and throws its reason, even while an event runs here; that run
770
+ finishes on its own.
771
+ - `ctx.session.consume` of a consumer that is waiting on the running one
772
+ throws a `TypeError`, unless that consumer has handled everything through
773
+ `through` already.
774
+ - A consumer without a string `lane`, one of another contract, a `through`
775
+ that isn't a non-negative safe integer, or `ctx.session.consume` of the
776
+ running consumer itself throws a `TypeError` before any I/O.
777
+ - On `server.session(id)` and `ctx.session`. `SessionConsumeOptions` and
778
+ `SessionConsumeResult` are exported from `experimental-a2/server`.
779
+
726
780
  ### `session.history()`
727
781
 
728
782
  ```ts
@@ -747,18 +801,18 @@ session.state(
747
801
  ```
748
802
 
749
803
  The log folded through one committed prefix. `index` is its exact boundary, an
750
- append-order cursor rather than a handler-settlement marker; hand it to the
804
+ append-order cursor rather than a consumer-progress marker; hand it to the
751
805
  client to resume there. See [State](/contract/state).
752
806
 
753
807
  - Omitting `through` reads the latest committed frontier on a root session,
754
808
  and through `ctx.event.index` on `ctx.session`, inclusive and stable across
755
- concurrent appends and retries. In a handler, use `{ through: 'latest' }`
809
+ concurrent appends and retries. In a consumer, use `{ through: 'latest' }`
756
810
  only when later committed events are intentionally part of the decision.
757
811
  - A numeric `through` is an inclusive, non-negative safe integer;
758
812
  `through: 0` returns the reducer's initial state.
759
813
  - A latest read and a following append are not atomic. See
760
814
  [State](/contract/state#reducers) for joins that converge.
761
- - Never dispatches handlers or waits for pending work. Snapshot write-back
815
+ - Never runs consumers or waits for pending work. Snapshot write-back
762
816
  uses platform `waitUntil`, and its failures never fail the completed read:
763
817
  they go to `a2.snapshot` telemetry or the default console sink, without
764
818
  snapshot contents.
@@ -767,11 +821,12 @@ client to resume there. See [State](/contract/state).
767
821
  positive `through` below `clearIndex` throws `SESSION_CLEARED`; `through: 0`
768
822
  returns the seed without `clearIndex`.
769
823
  - On `ctx.session`, every state read throws `SESSION_CLEARED` once the trigger
770
- has been cleared, including `{ through: 'latest' }`. Handler `history()` and
824
+ has been cleared, including `{ through: 'latest' }`. Its `history()` and
771
825
  `stream()` are not fenced.
772
- - Default and numeric handler reads retain their exact folded checkpoint until
773
- the trigger completes or dead-letters and no other unfinished handler uses
774
- it. Handler `{ through: 'latest' }` reads and root reads retain nothing.
826
+ - Default and numeric reads on `ctx.session` retain their exact folded
827
+ checkpoint until the trigger completes or dead-letters and no other
828
+ unfinished run uses it. Its `{ through: 'latest' }` reads and root reads
829
+ retain nothing.
775
830
  There are no retention options. A numeric read behind the latest snapshot
776
831
  can use the greatest eligible retained checkpoint. See
777
832
  [Snapshots are a cache](/contract/state#snapshots-are-a-cache).
@@ -791,7 +846,7 @@ session.stream(options: {
791
846
  ```
792
847
 
793
848
  A live feed of the session's events. Server-side only; `server.fetch` exposes
794
- it over SSE and WebSocket. Subscribing never dispatches handlers.
849
+ it over SSE and WebSocket. Subscribing never runs consumers.
795
850
 
796
851
  - `startAfter` is a non-negative safe integer; `startAfter: 20` begins with
797
852
  event 21.
@@ -854,7 +909,7 @@ session.presence(): Promise<{
854
909
  ```
855
910
 
856
911
  The current presence map, expired values pruned: a point-in-time read for
857
- server renders. Live consumers use `stream({ presence: true })`. Each entry's
912
+ server renders. Live views use `stream({ presence: true })`. Each entry's
858
913
  `at` is the sender's stamp for when the value was set, the last-writer-wins
859
914
  comparator; how long a value
860
915
  stays painted is view logic, decided at render time.