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
@@ -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.
@@ -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
@@ -505,11 +517,12 @@ The only way to move a session forward. See [Guarantees](/server/guarantees).
505
517
  - A multi-event append is atomic: all-or-nothing, consecutive positions, one
506
518
  transaction.
507
519
  - An explicit `id` makes it idempotent across retries; re-sending an identical
508
- batch returns the original rows.
520
+ batch returns the original rows. Ids are unique per session: the same `id`
521
+ in another session is another event.
509
522
  - 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
523
+ - Only events some consumer takes are dispatched; the append itself settles
524
+ the rest. An append no consumer takes still starts session healing when
525
+ older work is pending. The store reports that pending state in the
513
526
  atomic append, so the decision needs no follow-up read.
514
527
 
515
528
  ### `session.append.dispatch(...events)`
@@ -525,7 +538,7 @@ scheduler instead of starting an inline drain. See
525
538
  [Dispatch in a fresh invocation](/schedulers/overview#dispatch-in-a-fresh-invocation).
526
539
 
527
540
  - Awaits acceptance of an immediate scheduler message. A batch that leaves no
528
- pending handler work sends none.
541
+ pending consumer work sends none.
529
542
  - Throws before writing when the server has no scheduler.
530
543
  - If the send fails, the events are already durable: give them explicit IDs
531
544
  and retry the same dispatch.
@@ -546,7 +559,7 @@ session.schedule(
546
559
  ```
547
560
 
548
561
  Appends the events later through the configured scheduler, from root and
549
- handler sessions. Resolves after provider acceptance, not after append. `name`
562
+ consumer sessions. Resolves after provider acceptance, not after append. `name`
550
563
  must be non-empty, with at least one event. `ScheduleDelay`, `ScheduleTiming`,
551
564
  and `SessionSchedule` are exported from `experimental-a2/server`. See
552
565
  [Timers](/server/timers) for stale-timer guards, provider limits, and
@@ -554,7 +567,7 @@ deployment compatibility.
554
567
 
555
568
  | Timing | Meaning |
556
569
  | --- | --- |
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 |
570
+ | `{ 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
571
  | `{ at: Date }` | absolute delivery time; use it when a retried root call needs one target |
559
572
 
560
573
  - The template type checks the unit. At runtime the number must be an
@@ -563,9 +576,10 @@ deployment compatibility.
563
576
  once are rejected.
564
577
  - The target is the earliest useful delivery time. Adapters encode it in
565
578
  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
579
+ - The task id derives from the contract, session, and name. A consumer's
580
+ tasks add the trigger id, the consumer's name, and, once the session has
581
+ been cleared, the clear, so a trigger id reused in a new lifetime schedules
582
+ a new task. Omitted event ids
569
583
  derive from the task id and position; explicit ids win.
570
584
  - Reuse a name only for identical timing and events. Provider deduplication is
571
585
  an optimization; stable event ids make later duplicate deliveries converge in
@@ -575,11 +589,11 @@ deployment compatibility.
575
589
  functions, symbols, cycles, accessors, and non-plain prototypes are rejected;
576
590
  shared references become independent values.
577
591
  - 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
592
+ top-level append, outside any consumer's completion transaction, so a
593
+ schema transform produces the stored payload from the original input. The
594
+ events have `cause: null` and run through consumers, lanes, state, and
595
+ streams normally.
596
+ - A consumer's task belongs to its trigger's lifetime: if the session is cleared
583
597
  before delivery, it is acknowledged without appending. A root task joins
584
598
  whichever lifetime exists at delivery, and its name identifies one task
585
599
  across lifetimes, so put something from the new lifetime in the name when
@@ -611,10 +625,10 @@ and `index` is always the head after the call:
611
625
  | otherwise | `{ cleared: true, index }`, the new marker |
612
626
 
613
627
  - Every event at or below the marker is superseded: never dispatched again,
614
- never blocking its lane. Its running handler's appends throw
628
+ never blocking its lane. A run still working on one has its appends throw
615
629
  `SUPERSEDED_ATTEMPT` and its state reads throw `SESSION_CLEARED` rather than
616
630
  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
631
+ read bounded at the run's own event may return that event's state from
618
632
  memory until its `ctx.signal` aborts.
619
633
  - Pushes and appends after the marker start the new lifetime.
620
634
  - `clear()` then deletes the old events, their bookkeeping and event-id
@@ -624,8 +638,8 @@ and `index` is always the head after the call:
624
638
  - If deletion fails, the call rejects with the session already cleared; any
625
639
  later clear of the session, including a retry, finishes the deletion.
626
640
  - 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
641
+ - On `ctx.session`, the clear is fenced like the run's appends and ends the
642
+ run's own attempt: `ctx.signal` aborts with `SUPERSEDED_ATTEMPT` and
629
643
  returned events are discarded. Its deletion keeps running under the
630
644
  platform's `waitUntil`.
631
645
  - All built-in stores support clearing.
@@ -647,19 +661,19 @@ works as in `session.schedule`, with the same provider limits. See
647
661
  [Scheduling a clear](/contract/sessions#scheduling-a-clear).
648
662
 
649
663
  - 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.
664
+ the trigger id, the consumer's name, and any earlier clear inside a
665
+ consumer. Reuse a name only for the same timing.
652
666
  - Delivery runs `clear({ ifLastIndex })` and acknowledges after the deletion
653
667
  finishes. No event is appended; a stale or duplicate delivery changes
654
668
  nothing. Indexes never repeat, so a condition from an older lifetime never
655
669
  matches a newer one.
656
670
  - `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`.
671
+ head. The consumer's own appends and returned events move the head, so a
672
+ consumer that writes should use the index of its last `ctx.session.append`.
659
673
  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.
674
+ - A delay short enough to deliver while `run` still works can match before its
675
+ writes land; the clear then ends that attempt and discards its returned
676
+ events.
663
677
  - Each delivery attempt resumes the deletion. Vercel Queues redelivers until
664
678
  retention ends. QStash retries up to its `retries` budget; a deletion that
665
679
  outlasts them leaves the message in QStash's dead-letter queue, the rest of
@@ -672,6 +686,7 @@ works as in `session.schedule`, with the same provider limits. See
672
686
  session.deadLetters(): Promise<DeadLetter[]>
673
687
 
674
688
  type DeadLetter = Event & {
689
+ consumer: string
675
690
  lane: string | null
676
691
  attemptCount: number
677
692
  failureCount: number
@@ -680,35 +695,37 @@ type DeadLetter = Event & {
680
695
  }
681
696
  ```
682
697
 
683
- The session's dead-lettered events, oldest first, each with why it failed. See
684
- [Resolve a dead letter](/server/handlers#resolve-a-dead-letter).
698
+ The session's dead letters in event order, then by consumer, each with why it
699
+ failed. One event can be dead for one consumer and done for another. See
700
+ [Resolve a dead letter](/server/consumers#resolve-a-dead-letter).
685
701
 
686
702
  - `failureCount` counts the attempts that spent its budget: caught failures,
687
703
  and attempts that ended before they finished. `attemptCount` counts every
688
- claim. `lastError` is the last error a handler threw, stringified, or null
704
+ claim. `lastError` is the last error `run` threw, stringified, or null
689
705
  when every failed attempt ended before it finished.
690
706
  - Events at or below the latest clear marker are never listed.
691
707
  - Only on `server.session(id)`. `DeadLetter` is exported from
692
708
  `experimental-a2/server`.
693
709
 
694
- ### `session.redrive(index, options)`
710
+ ### `session.redrive(target, options)`
695
711
 
696
712
  ```ts
697
713
  session.redrive(
698
- index: number,
714
+ target: { consumer: string; index: number },
699
715
  options: { action: 'retry' | 'skip' },
700
716
  ): Promise<{ redriven: boolean }>
701
717
  ```
702
718
 
703
- Resolves the dead-lettered event at `index` in one atomic store operation.
719
+ Resolves the consumer's dead-lettered event at `index` in one atomic store
720
+ operation. Every dead letter names its consumer, A2's own included.
704
721
 
705
722
  | Action | Effect |
706
723
  | --- | --- |
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. |
724
+ | `retry` | The consumer runs the event again as a new attempt, with a fresh ten-failure budget. Once it completes, its lane continues. |
725
+ | `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
726
 
710
- - `index` is a positive safe integer and `action` is required; anything else
711
- throws a `TypeError` before any I/O.
727
+ - `consumer` is a consumer's name, `index` is a positive safe integer, and
728
+ `action` is required; anything else throws a `TypeError` before any I/O.
712
729
  - `redriven` is `false`, with nothing written, when the event isn't
713
730
  dead-lettered: still pending, already processed, already redriven, missing,
714
731
  or at or below the latest clear marker. Calling it again is safe.
@@ -717,12 +734,50 @@ Resolves the dead-lettered event at `index` in one atomic store operation.
717
734
  `schedulerArmTimeoutMs`. A failed send doesn't reject the call; it marks the
718
735
  `a2.redrive` span and leaves later appends, `server.drain()`, or another
719
736
  `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`.
737
+ - A retried head keeps its `attemptCount` and last failure, so the next run's
738
+ `ctx.attempt` keeps counting. A skip moves the consumer's cursor on, and its
739
+ next head starts with fresh counts.
723
740
  - Only on `server.session(id)`. `SessionRedriveOptions` and
724
741
  `SessionRedriveResult` are exported from `experimental-a2/server`.
725
742
 
743
+ ### `session.consume(consumer, options?)`
744
+
745
+ ```ts
746
+ session.consume(
747
+ consumer: Consumer,
748
+ options?: { through?: number; signal?: AbortSignal },
749
+ ): Promise<{ index: number }>
750
+ ```
751
+
752
+ Runs a consumer with a string `lane` on this session, here, until it has
753
+ handled every event it takes through `through`: by default the session's head
754
+ when the call starts, and never more than it. Returns the index it has handled
755
+ every event through, at least `through`. See
756
+ [Run a consumer now](/server/consumers#run-a-consumer-now-with-consume).
757
+
758
+ - The consumer's cursor is created on the first call when it's missing: at the
759
+ start of the log with `backfill`, otherwise at the head. An idle cursor also
760
+ finds events appended by deployments that don't run the consumer. A session
761
+ with nothing after its latest clear, or never written, gets no cursor: the
762
+ call writes nothing.
763
+ - The server's own consumer shares its cursor with background drains: a head
764
+ another invocation holds, or one waiting out a failure's backoff, is waited
765
+ for. Nothing past `through` runs here; it carries on in the background. A different
766
+ definition under one of the server's consumer names throws a `TypeError`.
767
+ - A consumer the server doesn't run is claimed only by `consume`, and keeps
768
+ its place between calls.
769
+ - A dead-lettered head throws `CONSUMER_DEAD_LETTERED`. `signal` stops the
770
+ wait and throws its reason, even while an event runs here; that run
771
+ finishes on its own.
772
+ - `ctx.session.consume` of a consumer that is waiting on the running one
773
+ throws a `TypeError`, unless that consumer has handled everything through
774
+ `through` already.
775
+ - A consumer without a string `lane`, one of another contract, a `through`
776
+ that isn't a non-negative safe integer, or `ctx.session.consume` of the
777
+ running consumer itself throws a `TypeError` before any I/O.
778
+ - On `server.session(id)` and `ctx.session`. `SessionConsumeOptions` and
779
+ `SessionConsumeResult` are exported from `experimental-a2/server`.
780
+
726
781
  ### `session.history()`
727
782
 
728
783
  ```ts
@@ -747,18 +802,18 @@ session.state(
747
802
  ```
748
803
 
749
804
  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
805
+ append-order cursor rather than a consumer-progress marker; hand it to the
751
806
  client to resume there. See [State](/contract/state).
752
807
 
753
808
  - Omitting `through` reads the latest committed frontier on a root session,
754
809
  and through `ctx.event.index` on `ctx.session`, inclusive and stable across
755
- concurrent appends and retries. In a handler, use `{ through: 'latest' }`
810
+ concurrent appends and retries. In a consumer, use `{ through: 'latest' }`
756
811
  only when later committed events are intentionally part of the decision.
757
812
  - A numeric `through` is an inclusive, non-negative safe integer;
758
813
  `through: 0` returns the reducer's initial state.
759
814
  - A latest read and a following append are not atomic. See
760
815
  [State](/contract/state#reducers) for joins that converge.
761
- - Never dispatches handlers or waits for pending work. Snapshot write-back
816
+ - Never runs consumers or waits for pending work. Snapshot write-back
762
817
  uses platform `waitUntil`, and its failures never fail the completed read:
763
818
  they go to `a2.snapshot` telemetry or the default console sink, without
764
819
  snapshot contents.
@@ -767,11 +822,12 @@ client to resume there. See [State](/contract/state).
767
822
  positive `through` below `clearIndex` throws `SESSION_CLEARED`; `through: 0`
768
823
  returns the seed without `clearIndex`.
769
824
  - On `ctx.session`, every state read throws `SESSION_CLEARED` once the trigger
770
- has been cleared, including `{ through: 'latest' }`. Handler `history()` and
825
+ has been cleared, including `{ through: 'latest' }`. Its `history()` and
771
826
  `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.
827
+ - Default and numeric reads on `ctx.session` retain their exact folded
828
+ checkpoint until the trigger completes or dead-letters and no other
829
+ unfinished run uses it. Its `{ through: 'latest' }` reads and root reads
830
+ retain nothing.
775
831
  There are no retention options. A numeric read behind the latest snapshot
776
832
  can use the greatest eligible retained checkpoint. See
777
833
  [Snapshots are a cache](/contract/state#snapshots-are-a-cache).
@@ -791,7 +847,7 @@ session.stream(options: {
791
847
  ```
792
848
 
793
849
  A live feed of the session's events. Server-side only; `server.fetch` exposes
794
- it over SSE and WebSocket. Subscribing never dispatches handlers.
850
+ it over SSE and WebSocket. Subscribing never runs consumers.
795
851
 
796
852
  - `startAfter` is a non-negative safe integer; `startAfter: 20` begins with
797
853
  event 21.
@@ -828,6 +884,9 @@ broadcasts. No log row, no dispatch, no scheduler arm; `history()` and
828
884
  epoch-ms stamp; the browser client stamps its sends automatically. Without
829
885
  `at`, receipt time stands in: a stampless caller is a single writer, so
830
886
  receipt order is its send order.
887
+ - `null` clears a field. The store keeps the clear's `at` until the field's
888
+ TTL runs out, so an older value that reaches it later, such as one sent
889
+ through another server instance, still loses.
831
890
  - A bad payload throws `INVALID_PAYLOAD`, an unknown field throws
832
891
  `UNKNOWN_PRESENCE_FIELD`, and a non-finite or negative `at` throws a
833
892
  `TypeError`.
@@ -854,7 +913,7 @@ session.presence(): Promise<{
854
913
  ```
855
914
 
856
915
  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
916
+ server renders. Live views use `stream({ presence: true })`. Each entry's
858
917
  `at` is the sender's stamp for when the value was set, the last-writer-wins
859
918
  comparator; how long a value
860
919
  stays painted is view logic, decided at render time.