experimental-a2 0.16.0 → 0.17.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 (334) hide show
  1. package/CHANGELOG.md +360 -0
  2. package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
  3. package/dist/actor-DBbe5xeE.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +2 -5
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +13 -4
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -3
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +20 -10
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  13. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  14. package/dist/actor.d.ts +1 -1
  15. package/dist/actor.js +42 -23
  16. package/dist/actor.js.map +1 -1
  17. package/dist/{ai-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
  18. package/dist/ai-UrWfKeuv.js.map +1 -0
  19. package/dist/ai-client.d.ts +27 -0
  20. package/dist/ai-client.d.ts.map +1 -0
  21. package/dist/ai-client.js +29 -0
  22. package/dist/ai-client.js.map +1 -0
  23. package/dist/ai-react.d.ts +96 -0
  24. package/dist/ai-react.d.ts.map +1 -0
  25. package/dist/ai-react.js +142 -0
  26. package/dist/ai-react.js.map +1 -0
  27. package/dist/ai-server.browser.js +2 -2
  28. package/dist/ai-server.browser.js.map +1 -1
  29. package/dist/ai-server.d.ts +80 -36
  30. package/dist/ai-server.d.ts.map +1 -1
  31. package/dist/ai-server.js +466 -265
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
  34. package/dist/ai-vpGtcaCB.d.ts.map +1 -0
  35. package/dist/ai.d.ts +2 -2
  36. package/dist/ai.js +2 -2
  37. package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
  38. package/dist/client-COaiaDub.js.map +1 -0
  39. package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
  40. package/dist/client-qRnRUzBS.d.ts.map +1 -0
  41. package/dist/client.d.ts +4 -2
  42. package/dist/client.js +1 -1
  43. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  44. package/dist/contract-BYKASlCV.js.map +1 -0
  45. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  46. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  47. package/dist/index.d.ts +6 -5
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -3
  50. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
  51. package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
  52. package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
  53. package/dist/internal-CbVvEwad.js.map +1 -0
  54. package/dist/otel.d.ts +1 -1
  55. package/dist/react-BbJ-g6Uu.d.ts +86 -0
  56. package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
  57. package/dist/react.d.ts +2 -122
  58. package/dist/react.js +107 -100
  59. package/dist/react.js.map +1 -1
  60. package/dist/reducer-BAXc8JyF.js +87 -0
  61. package/dist/reducer-BAXc8JyF.js.map +1 -0
  62. package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
  63. package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
  64. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  65. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +3 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +33 -10
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  71. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -9
  73. package/dist/scheduler-vercel.d.ts.map +1 -1
  74. package/dist/scheduler-vercel.js +25 -44
  75. package/dist/scheduler-vercel.js.map +1 -1
  76. package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
  77. package/dist/server-CGM8WzRG.d.ts.map +1 -0
  78. package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
  79. package/dist/server-Rj64fNvX.js.map +1 -0
  80. package/dist/server.d.ts +4 -4
  81. package/dist/server.js +2 -2
  82. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  83. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  84. package/dist/session-clear-Dshpuhff.d.ts +12 -0
  85. package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
  86. package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
  87. package/dist/store-Gpkq7WBo.d.ts.map +1 -0
  88. package/dist/store-N8PXxDAS.js.map +1 -1
  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 +182 -69
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
  94. package/dist/store-polling-4_y8lETm.js.map +1 -0
  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 +399 -108
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
  100. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.d.ts.map +1 -1
  103. package/dist/store-redis-http.js +293 -11
  104. package/dist/store-redis-http.js.map +1 -1
  105. package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
  106. package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
  107. package/dist/store-redis.d.ts +1 -1
  108. package/dist/store-redis.d.ts.map +1 -1
  109. package/dist/store-redis.js +11 -11
  110. package/dist/store-redis.js.map +1 -1
  111. package/dist/store-sqlite.d.ts +1 -1
  112. package/dist/store-sqlite.d.ts.map +1 -1
  113. package/dist/store-sqlite.js +166 -32
  114. package/dist/store-sqlite.js.map +1 -1
  115. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  116. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  117. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
  118. package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
  119. package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
  120. package/dist/wire-DjfHOvdW.js.map +1 -0
  121. package/docs/01-quickstart.mdx +2 -2
  122. package/docs/actors/01-introduction.mdx +4 -0
  123. package/docs/actors/04-routes.mdx +14 -1
  124. package/docs/concepts/01-contracts.mdx +95 -0
  125. package/docs/concepts/04-state.mdx +3 -3
  126. package/docs/guides/01-timers.mdx +22 -14
  127. package/docs/guides/02-cancellation.mdx +1 -1
  128. package/docs/guides/03-react.mdx +268 -328
  129. package/docs/guides/04-local-first.mdx +32 -73
  130. package/docs/guides/05-production.mdx +40 -27
  131. package/docs/guides/06-ai-agents.mdx +680 -580
  132. package/docs/guides/09-presence.mdx +3 -7
  133. package/docs/guides/10-transports.mdx +5 -5
  134. package/docs/index.mdx +10 -18
  135. package/docs/reference/01-api.mdx +662 -195
  136. package/docs/reference/02-errors.mdx +2 -1
  137. package/examples/playground/DEPLOY.md +52 -6
  138. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  139. package/examples/playground/app/a0/[appId]/page.tsx +48 -0
  140. package/examples/playground/app/a0/a0.css +818 -0
  141. package/examples/playground/app/a0/app-list.tsx +211 -0
  142. package/examples/playground/app/a0/app-view.tsx +55 -0
  143. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
  145. package/examples/playground/app/a0/apps.test.ts +116 -0
  146. package/examples/playground/app/a0/apps.ts +87 -0
  147. package/examples/playground/app/a0/chat.tsx +364 -0
  148. package/examples/playground/app/a0/columns.tsx +109 -0
  149. package/examples/playground/app/a0/context-menu.tsx +116 -0
  150. package/examples/playground/app/a0/cookies.ts +46 -0
  151. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  152. package/examples/playground/app/a0/crashes/route.ts +3 -0
  153. package/examples/playground/app/a0/crashes.test.ts +33 -0
  154. package/examples/playground/app/a0/crashes.ts +28 -0
  155. package/examples/playground/app/a0/devtools.tsx +164 -0
  156. package/examples/playground/app/a0/events/route.ts +6 -0
  157. package/examples/playground/app/a0/flags.ts +30 -0
  158. package/examples/playground/app/a0/layout.tsx +38 -0
  159. package/examples/playground/app/a0/list/route.ts +10 -0
  160. package/examples/playground/app/a0/model.test.ts +56 -0
  161. package/examples/playground/app/a0/model.ts +58 -0
  162. package/examples/playground/app/a0/page.tsx +14 -0
  163. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  164. package/examples/playground/app/a0/preview.test.ts +53 -0
  165. package/examples/playground/app/a0/preview.ts +26 -0
  166. package/examples/playground/app/a0/sandbox-log.ts +69 -0
  167. package/examples/playground/app/a0/sandbox.ts +191 -0
  168. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  169. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  170. package/examples/playground/app/a0/server.ts +158 -0
  171. package/examples/playground/app/a0/session.ts +47 -0
  172. package/examples/playground/app/a0/shell.tsx +150 -0
  173. package/examples/playground/app/a0/short/events/route.ts +9 -0
  174. package/examples/playground/app/a0/splitter.tsx +120 -0
  175. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  176. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  177. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  178. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  179. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  180. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  181. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  182. package/examples/playground/app/agent/events/route.ts +5 -2
  183. package/examples/playground/app/agent/ingress.test.ts +98 -0
  184. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  185. package/examples/playground/app/agent/model-input.tsx +115 -0
  186. package/examples/playground/app/agent/model.test.ts +73 -0
  187. package/examples/playground/app/agent/model.ts +58 -26
  188. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  189. package/examples/playground/app/agent/page.tsx +4 -4
  190. package/examples/playground/app/agent/server.ts +15 -14
  191. package/examples/playground/app/agent/session.ts +5 -6
  192. package/examples/playground/app/agent/telemetry.test.ts +168 -0
  193. package/examples/playground/app/agent/telemetry.ts +30 -0
  194. package/examples/playground/app/agent/trace.test.ts +23 -0
  195. package/examples/playground/app/agent/trace.ts +18 -0
  196. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
  197. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  198. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  199. package/examples/playground/app/canvas/model.ts +53 -53
  200. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  201. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  202. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  203. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  204. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  205. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  206. package/examples/playground/app/chat/agent/route.ts +9 -0
  207. package/examples/playground/app/chat/model.test.ts +46 -119
  208. package/examples/playground/app/chat/model.ts +82 -128
  209. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  210. package/examples/playground/app/chat/server.test.ts +108 -0
  211. package/examples/playground/app/chat/server.ts +69 -73
  212. package/examples/playground/app/components/activity-feed.tsx +30 -1
  213. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  214. package/examples/playground/app/components/demo-icons.tsx +89 -0
  215. package/examples/playground/app/components/demos.tsx +33 -0
  216. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  217. package/examples/playground/app/components/fast-link.tsx +55 -0
  218. package/examples/playground/app/components/site-header.tsx +96 -0
  219. package/examples/playground/app/counter/counter-client.tsx +3 -3
  220. package/examples/playground/app/counter/model.test.ts +3 -3
  221. package/examples/playground/app/counter/model.ts +11 -21
  222. package/examples/playground/app/counter/page.tsx +2 -15
  223. package/examples/playground/app/counter/session.ts +2 -2
  224. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  225. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  226. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  227. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  228. package/examples/playground/app/documents/model.ts +27 -27
  229. package/examples/playground/app/globals.css +420 -207
  230. package/examples/playground/app/initial-state.test.tsx +54 -0
  231. package/examples/playground/app/layout.tsx +7 -23
  232. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  233. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  234. package/examples/playground/app/orders/model.ts +49 -49
  235. package/examples/playground/app/page.tsx +13 -102
  236. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  237. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  238. package/examples/playground/app/recovery/model.ts +30 -30
  239. package/examples/playground/instrumentation.ts +5 -0
  240. package/examples/playground/package.json +10 -2
  241. package/examples/playground/scripts/a0-template.ts +77 -0
  242. package/examples/playground/vercel.json +18 -0
  243. package/package.json +3 -1
  244. package/src/actor-client.ts +16 -4
  245. package/src/actor-react.ts +25 -11
  246. package/src/actor-shared.ts +21 -6
  247. package/src/actor.ts +76 -31
  248. package/src/ai-agent-actions.ts +219 -0
  249. package/src/ai-agent.ts +2343 -0
  250. package/src/ai-app-state.ts +91 -0
  251. package/src/ai-client-state.ts +39 -2
  252. package/src/ai-client.ts +62 -0
  253. package/src/ai-context-schema.ts +25 -5
  254. package/src/ai-context.ts +90 -40
  255. package/src/ai-control-server.ts +94 -55
  256. package/src/ai-control.ts +2 -0
  257. package/src/ai-coordinator.ts +23 -22
  258. package/src/ai-message-base.ts +209 -0
  259. package/src/ai-message-projection.ts +382 -58
  260. package/src/ai-progress-batches.ts +26 -0
  261. package/src/ai-projector.ts +95 -14
  262. package/src/ai-react.ts +146 -0
  263. package/src/ai-server.browser.ts +2 -2
  264. package/src/ai-server.ts +20 -1951
  265. package/src/ai-stored-state.ts +48 -163
  266. package/src/ai.ts +343 -276
  267. package/src/bootstrap-stream.ts +72 -0
  268. package/src/bounded-cache.ts +55 -0
  269. package/src/client-render-session.ts +49 -0
  270. package/src/client.ts +688 -151
  271. package/src/contract.ts +92 -42
  272. package/src/deterministic-id.ts +37 -0
  273. package/src/equal-json.ts +32 -0
  274. package/src/errors.ts +13 -1
  275. package/src/index.ts +5 -1
  276. package/src/initial-snapshot.ts +39 -0
  277. package/src/internal.ts +45 -0
  278. package/src/postgres-notification-scope.ts +14 -3
  279. package/src/react.ts +171 -206
  280. package/src/redis-http-reply.ts +256 -0
  281. package/src/redis-http-stream.ts +73 -0
  282. package/src/redis-http-subscriptions.ts +13 -5
  283. package/src/reducer-runtime.ts +52 -0
  284. package/src/reducer.ts +92 -10
  285. package/src/scheduler-qstash.ts +49 -20
  286. package/src/scheduler-task.ts +34 -2
  287. package/src/scheduler-vercel.ts +32 -84
  288. package/src/server-fetch.ts +139 -35
  289. package/src/server.ts +957 -237
  290. package/src/session-clear.ts +23 -0
  291. package/src/session-socket.ts +154 -49
  292. package/src/sse.ts +71 -4
  293. package/src/store-memory.ts +220 -78
  294. package/src/store-polling.ts +18 -4
  295. package/src/store-postgres.ts +461 -114
  296. package/src/store-redis-core.ts +1260 -434
  297. package/src/store-redis-http.ts +18 -2
  298. package/src/store-redis-notify.ts +260 -85
  299. package/src/store-redis.ts +19 -9
  300. package/src/store-sqlite.ts +259 -45
  301. package/src/store.ts +46 -1
  302. package/src/stream-buffer.ts +43 -0
  303. package/src/telemetry.ts +13 -1
  304. package/src/wire.ts +241 -13
  305. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  306. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  307. package/dist/ai-CrLYwNEx.js.map +0 -1
  308. package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
  309. package/dist/client-BYzHjkwU.js.map +0 -1
  310. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  311. package/dist/contract-CKRg_E4q.js.map +0 -1
  312. package/dist/errors-DCk6ch5n.js.map +0 -1
  313. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  314. package/dist/internal-Dq2qYxou.js.map +0 -1
  315. package/dist/react.d.ts.map +0 -1
  316. package/dist/reducer-DEMjEY_O.js +0 -29
  317. package/dist/reducer-DEMjEY_O.js.map +0 -1
  318. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  319. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  320. package/dist/server-BD5ckxZb.js.map +0 -1
  321. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  322. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  323. package/dist/store-polling-CmxUbV93.js.map +0 -1
  324. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  325. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  326. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  327. package/dist/validate-XKT4FSNn.js +0 -30
  328. package/dist/validate-XKT4FSNn.js.map +0 -1
  329. package/dist/wire-BO5wWCb1.js.map +0 -1
  330. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  331. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  332. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  333. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  334. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -7,9 +7,10 @@
7
7
  */
8
8
 
9
9
  import type { PresencePatch } from './contract.ts'
10
- import { A2Error } from './errors.ts'
10
+ import { A2Error, supersededAttemptError } from './errors.ts'
11
11
  import { idempotentReplay } from './idempotent-replay.ts'
12
12
  import { nullProtoRecord } from './internal.ts'
13
+ import { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'
13
14
  import {
14
15
  RANDOM_IDS,
15
16
  SYSTEM_CLOCK,
@@ -68,6 +69,20 @@ type SessionDispatch = {
68
69
  lanes: Map<string, LaneQueue>
69
70
  }
70
71
 
72
+ /** Contiguous rows; `reclaim` removes from the front below `clearIndex`. */
73
+ type SessionLog = {
74
+ rows: Row[]
75
+ /** Index of the event before `rows[0]`. */
76
+ offset: number
77
+ /** Latest clear marker, or 0. */
78
+ clearIndex: number
79
+ }
80
+
81
+ const RECLAIM_BATCH = 256
82
+
83
+ const headOf = (log: SessionLog | undefined): number =>
84
+ log ? log.offset + log.rows.length : 0
85
+
71
86
  const toStored = (row: Row): StoredEvent => ({
72
87
  id: row.id,
73
88
  type: row.type,
@@ -111,7 +126,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
111
126
  const clock = options.clock ?? SYSTEM_CLOCK
112
127
  const generateId = options.ids ?? RANDOM_IDS
113
128
 
114
- const sessions = new Map<string, Row[]>()
129
+ const sessions = new Map<string, SessionLog>()
115
130
  const dispatch = new Map<string, SessionDispatch>()
116
131
  /** Global unique index on event ids, like the SQL schema's. */
117
132
  const byEventId = new Map<string, Row>()
@@ -145,13 +160,31 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
145
160
  }
146
161
  }
147
162
 
148
- const rowsOf = (sessionId: string): Row[] => {
149
- let rows = sessions.get(sessionId)
150
- if (!rows) {
151
- rows = []
152
- sessions.set(sessionId, rows)
163
+ const logOf = (sessionId: string): SessionLog => {
164
+ let log = sessions.get(sessionId)
165
+ if (!log) {
166
+ log = { rows: [], offset: 0, clearIndex: 0 }
167
+ sessions.set(sessionId, log)
153
168
  }
154
- return rows
169
+ return log
170
+ }
171
+
172
+ const rowAt = (sessionId: string, index: number): Row | undefined => {
173
+ const log = sessions.get(sessionId)
174
+ return log && index > log.offset
175
+ ? log.rows[index - log.offset - 1]
176
+ : undefined
177
+ }
178
+
179
+ const clearIndexOf = (sessionId: string): number =>
180
+ sessions.get(sessionId)?.clearIndex ?? 0
181
+
182
+ /** The row owning `id`, ignoring this session's rows below its clear. */
183
+ const ownerOf = (sessionId: string, id: string): Row | undefined => {
184
+ const row = byEventId.get(id)
185
+ return row?.sessionId === sessionId && row.index < clearIndexOf(sessionId)
186
+ ? undefined
187
+ : row
155
188
  }
156
189
 
157
190
  const checkpointKey = (
@@ -217,7 +250,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
217
250
  }
218
251
 
219
252
  const find = (sessionId: string, index: number): Row => {
220
- const row = sessions.get(sessionId)?.[index - 1]
253
+ const row = rowAt(sessionId, index)
221
254
  if (!row) {
222
255
  throw new TypeError(
223
256
  `no event at index ${index} in session '${sessionId}'`,
@@ -226,12 +259,26 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
226
259
  return row
227
260
  }
228
261
 
229
- const appendResult = (sessionId: string, events: StoredEvent[]) => ({
230
- events,
231
- hasPending: (sessions.get(sessionId) ?? []).some(
232
- (row) => row.processedAt === null,
233
- ),
234
- })
262
+ const appendResult = (sessionId: string, events: StoredEvent[]) => {
263
+ const log = sessions.get(sessionId)
264
+ return {
265
+ events,
266
+ hasPending: (log?.rows ?? []).some(
267
+ (row) => row.index > log!.clearIndex && row.processedAt === null,
268
+ ),
269
+ }
270
+ }
271
+
272
+ /** The attempt-currency fence shared by handler appends and clears. */
273
+ const assertCurrentCause = (sessionId: string, cause: EventCause): void => {
274
+ if (cause.index <= clearIndexOf(sessionId)) {
275
+ throw supersededAttemptError(sessionId, cause)
276
+ }
277
+ const parent = find(sessionId, cause.index)
278
+ if (parent.attemptCount !== cause.attempt || parent.failedAt !== null) {
279
+ throw supersededAttemptError(sessionId, cause)
280
+ }
281
+ }
235
282
 
236
283
  const claim = (
237
284
  row: Row,
@@ -263,7 +310,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
263
310
  )
264
311
  }
265
312
  const existing = supplied
266
- .map((candidate) => byEventId.get(candidate.id))
313
+ .map((candidate) => ownerOf(sessionId, candidate.id))
267
314
  .filter((row): row is Row => row !== undefined)
268
315
  const foreign = existing.find((row) => row.sessionId !== sessionId)
269
316
  if (foreign) {
@@ -281,8 +328,8 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
281
328
  now: Date,
282
329
  cause?: EventCause,
283
330
  ): Row[] => {
284
- const rows = rowsOf(sessionId)
285
- const base = rows.length === 0 ? 0 : rows[rows.length - 1]!.index
331
+ const log = logOf(sessionId)
332
+ const base = headOf(log)
286
333
  const inserted = events.map((candidate, offset): Row => ({
287
334
  id: candidate.id ?? generateId(),
288
335
  type: candidate.type,
@@ -311,7 +358,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
311
358
  failedAt: null,
312
359
  }))
313
360
  for (const row of inserted) {
314
- if (byEventId.has(row.id)) {
361
+ if (ownerOf(sessionId, row.id)) {
315
362
  throw new A2Error(
316
363
  'PARTIAL_DUPLICATE_BATCH',
317
364
  `event id '${row.id}' already exists`,
@@ -319,13 +366,31 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
319
366
  }
320
367
  }
321
368
  for (const row of inserted) {
322
- rows.push(row)
369
+ log.rows.push(row)
323
370
  byEventId.set(row.id, row)
324
371
  enqueue(sessionId, row)
325
372
  }
326
373
  return inserted
327
374
  }
328
375
 
376
+ /** Every cached fold of this session below its clear, head or historical. */
377
+ const dropCheckpointsBelow = (
378
+ sessionId: string,
379
+ clearIndex: number,
380
+ ): void => {
381
+ const prefix = `${sessionId}\u0000`
382
+ for (const [key, snapshot] of snapshots) {
383
+ if (!key.startsWith(prefix) || snapshot.index >= clearIndex) continue
384
+ snapshots.delete(key)
385
+ snapshotPinCounts.delete(`${key}\u0000${snapshot.index}`)
386
+ }
387
+ for (const [key, snapshot] of historicalSnapshots) {
388
+ if (!key.startsWith(prefix) || snapshot.index >= clearIndex) continue
389
+ historicalSnapshots.delete(key)
390
+ snapshotPinCounts.delete(key)
391
+ }
392
+ }
393
+
329
394
  const readStateOf = (
330
395
  sessionId: string,
331
396
  reducerName: string,
@@ -333,8 +398,10 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
333
398
  snapshotThroughIndex?: number,
334
399
  ): StoreStateRead => {
335
400
  const key = snapshotKey(sessionId, reducerName)
401
+ const clearIndex = clearIndexOf(sessionId)
336
402
  const snapshotFrontier = snapshotThroughIndex ?? throughIndex
337
- const head = snapshots.get(key)
403
+ const stored = snapshots.get(key)
404
+ const head = stored && stored.index >= clearIndex ? stored : undefined
338
405
  let candidate =
339
406
  head && (snapshotFrontier === undefined || head.index <= snapshotFrontier)
340
407
  ? head
@@ -344,6 +411,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
344
411
  for (const [historicalKey, snapshot] of historicalSnapshots) {
345
412
  if (
346
413
  historicalKey.startsWith(prefix) &&
414
+ snapshot.index >= clearIndex &&
347
415
  snapshot.index <= snapshotFrontier &&
348
416
  (!candidate || snapshot.index > candidate.index)
349
417
  ) {
@@ -354,22 +422,23 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
354
422
  const snapshot = candidate
355
423
  ? { index: candidate.index, state: structuredClone(candidate.state) }
356
424
  : null
357
- const afterIndex = snapshot?.index ?? 0
425
+ const afterIndex = Math.max(snapshot?.index ?? 0, clearIndex - 1)
358
426
  return {
359
427
  headIndex: head?.index ?? null,
360
428
  snapshot,
361
- events: (sessions.get(sessionId) ?? [])
429
+ events: (sessions.get(sessionId)?.rows ?? [])
362
430
  .filter(
363
431
  (row) =>
364
432
  row.index > afterIndex &&
365
433
  (throughIndex === undefined || row.index <= throughIndex),
366
434
  )
367
435
  .map(toEvent),
436
+ ...(clearIndex > 0 ? { clearIndex } : {}),
368
437
  }
369
438
  }
370
439
 
371
440
  return {
372
- async append(sessionId, events) {
441
+ async append(sessionId, events, appendOptions) {
373
442
  if (events.length === 0) return appendResult(sessionId, [])
374
443
 
375
444
  // Batch-scoped idempotency (a2-implementation.md §3): all ids
@@ -389,26 +458,16 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
389
458
  )
390
459
  }
391
460
 
461
+ const originIndex = appendOptions?.originIndex
462
+ if (originIndex !== undefined && originIndex <= clearIndexOf(sessionId)) {
463
+ throw sessionClearedError(
464
+ `scheduled work from event ${originIndex} was cleared in session '${sessionId}'`,
465
+ )
466
+ }
392
467
  // Attempt-currency fence: a fresh handler append commits only while
393
468
  // its causal attempt is still the parent's latest (store.ts `append`).
394
- const rows = sessions.get(sessionId) ?? []
395
469
  for (const event of events) {
396
- if (!event.cause) continue
397
- const parent = rows[event.cause.index - 1]
398
- if (!parent) {
399
- throw new TypeError(
400
- `no event at index ${event.cause.index} in session '${sessionId}'`,
401
- )
402
- }
403
- if (
404
- parent.attemptCount !== event.cause.attempt ||
405
- parent.failedAt !== null
406
- ) {
407
- throw new A2Error(
408
- 'SUPERSEDED_ATTEMPT',
409
- `attempt ${event.cause.attempt} no longer owns event ${event.cause.index} in session '${sessionId}'`,
410
- )
411
- }
470
+ if (event.cause) assertCurrentCause(sessionId, event.cause)
412
471
  }
413
472
 
414
473
  const now = clock.now()
@@ -417,11 +476,67 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
417
476
  return appendResult(sessionId, inserted.map(toStored))
418
477
  },
419
478
 
479
+ async clear({ sessionId, ifLastIndex, cause }) {
480
+ if (cause) assertCurrentCause(sessionId, cause)
481
+ const log = sessions.get(sessionId)
482
+ const head = headOf(log)
483
+ const clearIndex = log?.clearIndex ?? 0
484
+ if (ifLastIndex !== undefined && clearIndex === ifLastIndex + 1) {
485
+ return { cleared: true, index: head, clearIndex }
486
+ }
487
+ if (ifLastIndex !== undefined && ifLastIndex !== head) {
488
+ return { cleared: false, index: head, clearIndex }
489
+ }
490
+ if (head === 0 || clearIndex === head) {
491
+ return { cleared: true, index: head, clearIndex }
492
+ }
493
+ const [marker] = insert(
494
+ sessionId,
495
+ [{ type: CLEAR_EVENT_TYPE, payload: null, settled: true }],
496
+ clock.now(),
497
+ )
498
+ logOf(sessionId).clearIndex = marker!.index
499
+ // Every row is at or below the marker, so no dispatch state survives.
500
+ dispatch.delete(sessionId)
501
+ notifyStreams(sessionId, [marker!])
502
+ return { cleared: true, index: marker!.index, clearIndex: marker!.index }
503
+ },
504
+
505
+ async reclaim(sessionId) {
506
+ const log = sessions.get(sessionId)
507
+ if (!log || log.offset >= log.clearIndex - 1) {
508
+ return { deleted: 0, done: true }
509
+ }
510
+ const removed = log.rows.splice(
511
+ 0,
512
+ Math.min(RECLAIM_BATCH, log.clearIndex - 1 - log.offset),
513
+ )
514
+ log.offset += removed.length
515
+ for (const row of removed) {
516
+ // A reused id belongs to its new row now.
517
+ if (byEventId.get(row.id) === row) byEventId.delete(row.id)
518
+ releaseSnapshotPins(sessionId, row.index)
519
+ }
520
+ const done = log.offset === log.clearIndex - 1
521
+ // Writes below the marker are dropped, so one sweep at the end suffices.
522
+ if (done) dropCheckpointsBelow(sessionId, log.clearIndex)
523
+ return { deleted: removed.length, done }
524
+ },
525
+
420
526
  async read(sessionId, opts) {
421
- const rows = sessions.get(sessionId) ?? []
422
- const start = Math.max(0, opts?.afterIndex ?? 0)
423
- const end = Math.max(0, opts?.throughIndex ?? rows.length)
424
- return rows.slice(start, end).map(toStored)
527
+ const log = sessions.get(sessionId)
528
+ if (!log) return []
529
+ const head = headOf(log)
530
+ const after = Math.max(
531
+ opts?.afterIndex ?? 0,
532
+ log.clearIndex - 1,
533
+ log.offset,
534
+ )
535
+ const through = Math.min(opts?.throughIndex ?? head, head)
536
+ if (through <= after) return []
537
+ return log.rows
538
+ .slice(after - log.offset, through - log.offset)
539
+ .map(toStored)
425
540
  },
426
541
 
427
542
  async claimAvailable({
@@ -453,21 +568,32 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
453
568
  return retryAt ? { outcome: 'busy', retryAt } : { outcome: 'settled' }
454
569
  }
455
570
  eligible.sort((a, b) => a.index - b.index)
571
+ const clearIndex = clearIndexOf(sessionId)
456
572
  return {
457
573
  outcome: 'claimed',
458
- events: eligible.map((row) => claim(row, holder, claimedAt, expiresAt)),
574
+ events: eligible.map((row) => {
575
+ const claimed = claim(row, holder, claimedAt, expiresAt)
576
+ if (clearIndex > 0) claimed.clearIndex = clearIndex
577
+ return claimed
578
+ }),
459
579
  }
460
580
  },
461
581
 
462
582
  async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
463
583
  const now = clock.now()
464
584
  const expiresAt = new Date(expiresAtMs ?? now.getTime() + ttlMs)
465
- const requested = new Map(claims.map((c) => [c.index, c.attempt]))
585
+ const clearIndex = clearIndexOf(sessionId)
466
586
  const renewed: number[] = []
467
587
  const superseded: number[] = []
468
- for (const row of sessions.get(sessionId) ?? []) {
469
- const attempt = requested.get(row.index)
470
- if (attempt === undefined) continue
588
+ for (const { index, attempt } of claims.toSorted(
589
+ (a, b) => a.index - b.index,
590
+ )) {
591
+ if (index <= clearIndex) {
592
+ superseded.push(index)
593
+ continue
594
+ }
595
+ const row = rowAt(sessionId, index)
596
+ if (!row) continue
471
597
  if (row.attemptCount > attempt) {
472
598
  superseded.push(row.index)
473
599
  continue
@@ -487,6 +613,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
487
613
  },
488
614
 
489
615
  async completeAttempt({ sessionId, index, attempt, events }) {
616
+ if (index <= clearIndexOf(sessionId)) return { outcome: 'superseded' }
490
617
  const parent = find(sessionId, index)
491
618
  const cause = { index, attempt }
492
619
  if (parent.processedAt !== null) {
@@ -553,6 +680,9 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
553
680
  },
554
681
 
555
682
  async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
683
+ if (index <= clearIndexOf(sessionId)) {
684
+ return { outcome: 'superseded', failureCount: 0 }
685
+ }
556
686
  const row = find(sessionId, index)
557
687
  if (
558
688
  row.processedAt ||
@@ -611,11 +741,18 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
611
741
 
612
742
  async putSnapshots(sessionId, reducerName, writes) {
613
743
  const key = snapshotKey(sessionId, reducerName)
744
+ const clearIndex = clearIndexOf(sessionId)
614
745
  for (const write of writes.toSorted((a, b) => a.index - b.index)) {
746
+ if (write.index < clearIndex) continue
615
747
  const checkpoint = checkpointKey(sessionId, reducerName, write.index)
616
748
  for (const eventIndex of write.pinEventIndexes ?? []) {
617
- const event = sessions.get(sessionId)?.[eventIndex - 1]
618
- if (!event || event.processedAt !== null || event.failedAt !== null) {
749
+ const event = rowAt(sessionId, eventIndex)
750
+ if (
751
+ !event ||
752
+ event.index <= clearIndex ||
753
+ event.processedAt !== null ||
754
+ event.failedAt !== null
755
+ ) {
619
756
  continue
620
757
  }
621
758
  const pinKey = eventPinKey(sessionId, eventIndex)
@@ -635,7 +772,11 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
635
772
 
636
773
  const current = snapshots.get(key)
637
774
  if (!current || write.index >= current.index) {
638
- if (current && write.index > current.index) {
775
+ if (
776
+ current &&
777
+ write.index > current.index &&
778
+ current.index >= clearIndex
779
+ ) {
639
780
  const currentCheckpoint = checkpointKey(
640
781
  sessionId,
641
782
  reducerName,
@@ -769,13 +910,11 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
769
910
  const startAfter = opts?.startAfter ?? 0
770
911
  return {
771
912
  [Symbol.asyncIterator](): AsyncIterator<Event> {
772
- let last = startAfter
773
- const buffer: Row[] = []
913
+ let last = Math.max(startAfter, clearIndexOf(sessionId) - 1)
774
914
  let wake: (() => void) | null = null
775
915
  let closed = false
776
916
 
777
- const onRow = (row: Row): void => {
778
- buffer.push(row)
917
+ const onRow = (): void => {
779
918
  wake?.()
780
919
  }
781
920
  let subs = streamSubscribers.get(sessionId)
@@ -784,37 +923,40 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
784
923
  streamSubscribers.set(sessionId, subs)
785
924
  }
786
925
  subs.add(onRow)
787
- // Seed with history — synchronously, in the same tick as the
788
- // subscription, so nothing can slip between the two.
789
- const existing = sessions.get(sessionId) ?? []
790
- buffer.unshift(...existing.filter((row) => row.index > startAfter))
791
-
792
- const unsubscribe = (): void => {
926
+ const stop = (): void => {
927
+ if (closed) return
928
+ closed = true
793
929
  subs.delete(onRow)
794
930
  if (subs.size === 0) streamSubscribers.delete(sessionId)
931
+ wake?.()
795
932
  }
796
933
 
797
934
  return {
798
935
  async next(): Promise<IteratorResult<Event>> {
799
- for (;;) {
800
- while (buffer.length > 0) {
801
- const row = buffer.shift()!
802
- if (row.index <= last) continue
803
- last = row.index
804
- return { value: toStored(row), done: false }
936
+ try {
937
+ for (;;) {
938
+ if (closed) return { value: undefined, done: true }
939
+ const log = sessions.get(sessionId)
940
+ // Rows reclaimed behind a running stream are skipped.
941
+ const row = log?.rows[Math.max(0, last - log.offset)]
942
+ if (row) {
943
+ const value = toStored(row)
944
+ last = row.index
945
+ return { value, done: false }
946
+ }
947
+ // oxlint-disable-next-line no-await-in-loop -- wait-for-wake
948
+ await new Promise<void>((resolve) => {
949
+ wake = resolve
950
+ })
951
+ wake = null
805
952
  }
806
- if (closed) return { value: undefined, done: true }
807
- // oxlint-disable-next-line no-await-in-loop -- wait-for-wake
808
- await new Promise<void>((resolve) => {
809
- wake = resolve
810
- })
811
- wake = null
953
+ } catch (error) {
954
+ stop()
955
+ throw error
812
956
  }
813
957
  },
814
958
  async return(): Promise<IteratorResult<Event>> {
815
- closed = true
816
- unsubscribe()
817
- wake?.()
959
+ stop()
818
960
  return { value: undefined, done: true }
819
961
  },
820
962
  }
@@ -66,7 +66,8 @@ export function pollingStream(
66
66
  return {
67
67
  [Symbol.asyncIterator](): AsyncIterator<Event> {
68
68
  let last = startAfter
69
- let buffer: Event[] = []
69
+ let buffer: Array<Event | undefined> = []
70
+ let position = 0
70
71
  let closed = false
71
72
  let pending: Sleeper | null = null
72
73
  let interval = floor()
@@ -83,18 +84,29 @@ export function pollingStream(
83
84
  for (;;) {
84
85
  if (closed) return { value: undefined, done: true }
85
86
  try {
86
- // oxlint-disable-next-line no-await-in-loop -- poll loop
87
- if (buffer.length === 0) buffer = await readAfter(last)
87
+ if (position === buffer.length) {
88
+ // oxlint-disable-next-line no-await-in-loop -- poll loop
89
+ const rows = await readAfter(last)
90
+ if (closed) return { value: undefined, done: true }
91
+ buffer = rows
92
+ position = 0
93
+ }
88
94
  } catch (error) {
89
95
  if (closed) return { value: undefined, done: true }
90
96
  throw error
91
97
  }
92
98
  if (closed) return { value: undefined, done: true }
93
- const row = buffer.shift()
99
+ const row = buffer[position]
94
100
  if (row) {
95
101
  // Activity: tighten back to the floor.
96
102
  activity.activate()
97
103
  last = row.index
104
+ buffer[position] = undefined
105
+ position += 1
106
+ if (position === buffer.length) {
107
+ buffer = []
108
+ position = 0
109
+ }
98
110
  return { value: row, done: false }
99
111
  }
100
112
  // Empty poll: wait, then back off toward the idle ceiling.
@@ -111,6 +123,8 @@ export function pollingStream(
111
123
  },
112
124
  async return(): Promise<IteratorResult<Event>> {
113
125
  closed = true
126
+ buffer = []
127
+ position = 0
114
128
  pending?.cancel()
115
129
  return { value: undefined, done: true }
116
130
  },