experimental-a2 0.16.1 → 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 (333) hide show
  1. package/CHANGELOG.md +352 -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-DgOBltJ_.js → ai-UrWfKeuv.js} +621 -216
  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 +432 -258
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-CrEf6p_W.d.ts → ai-vpGtcaCB.d.ts} +114 -100
  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 +671 -581
  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 +656 -194
  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 +6 -3
  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 +92 -72
  256. package/src/ai-coordinator.ts +23 -22
  257. package/src/ai-message-base.ts +209 -0
  258. package/src/ai-message-projection.ts +262 -64
  259. package/src/ai-progress-batches.ts +26 -0
  260. package/src/ai-projector.ts +95 -14
  261. package/src/ai-react.ts +146 -0
  262. package/src/ai-server.browser.ts +2 -2
  263. package/src/ai-server.ts +20 -1964
  264. package/src/ai-stored-state.ts +48 -163
  265. package/src/ai.ts +339 -155
  266. package/src/bootstrap-stream.ts +72 -0
  267. package/src/bounded-cache.ts +55 -0
  268. package/src/client-render-session.ts +49 -0
  269. package/src/client.ts +688 -151
  270. package/src/contract.ts +92 -42
  271. package/src/deterministic-id.ts +37 -0
  272. package/src/equal-json.ts +32 -0
  273. package/src/errors.ts +13 -1
  274. package/src/index.ts +5 -1
  275. package/src/initial-snapshot.ts +39 -0
  276. package/src/internal.ts +45 -0
  277. package/src/postgres-notification-scope.ts +14 -3
  278. package/src/react.ts +171 -206
  279. package/src/redis-http-reply.ts +256 -0
  280. package/src/redis-http-stream.ts +73 -0
  281. package/src/redis-http-subscriptions.ts +13 -5
  282. package/src/reducer-runtime.ts +52 -0
  283. package/src/reducer.ts +92 -10
  284. package/src/scheduler-qstash.ts +49 -20
  285. package/src/scheduler-task.ts +34 -2
  286. package/src/scheduler-vercel.ts +32 -84
  287. package/src/server-fetch.ts +139 -35
  288. package/src/server.ts +957 -237
  289. package/src/session-clear.ts +23 -0
  290. package/src/session-socket.ts +154 -49
  291. package/src/sse.ts +71 -4
  292. package/src/store-memory.ts +220 -78
  293. package/src/store-polling.ts +18 -4
  294. package/src/store-postgres.ts +461 -114
  295. package/src/store-redis-core.ts +1260 -434
  296. package/src/store-redis-http.ts +18 -2
  297. package/src/store-redis-notify.ts +260 -85
  298. package/src/store-redis.ts +19 -9
  299. package/src/store-sqlite.ts +259 -45
  300. package/src/store.ts +46 -1
  301. package/src/stream-buffer.ts +43 -0
  302. package/src/telemetry.ts +13 -1
  303. package/src/wire.ts +241 -13
  304. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  305. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  306. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  307. package/dist/ai-DgOBltJ_.js.map +0 -1
  308. package/dist/client-BYzHjkwU.js.map +0 -1
  309. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  310. package/dist/contract-CKRg_E4q.js.map +0 -1
  311. package/dist/errors-DCk6ch5n.js.map +0 -1
  312. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  313. package/dist/internal-Dq2qYxou.js.map +0 -1
  314. package/dist/react.d.ts.map +0 -1
  315. package/dist/reducer-DEMjEY_O.js +0 -29
  316. package/dist/reducer-DEMjEY_O.js.map +0 -1
  317. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  318. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  319. package/dist/server-BD5ckxZb.js.map +0 -1
  320. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  321. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  322. package/dist/store-polling-CmxUbV93.js.map +0 -1
  323. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  324. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  325. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  326. package/dist/validate-XKT4FSNn.js +0 -30
  327. package/dist/validate-XKT4FSNn.js.map +0 -1
  328. package/dist/wire-BO5wWCb1.js.map +0 -1
  329. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  330. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  331. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  332. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  333. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -1,6 +1,6 @@
1
- import { t as A2Error } from "./errors-DCk6ch5n.js";
2
- import { n as pollingStream } from "./store-polling-CmxUbV93.js";
3
- import { t as idempotentReplay } from "./idempotent-replay-DVOlyYbx.js";
1
+ import { i as A2Error, r as sessionClearedError, s as supersededAttemptError, t as CLEAR_EVENT_TYPE } from "./session-clear-DGI4jTQ5.js";
2
+ import { n as pollingStream } from "./store-polling-4_y8lETm.js";
3
+ import { t as idempotentReplay } from "./idempotent-replay-JdgMQRRm.js";
4
4
  import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
5
5
  import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
6
6
  import { mkdirSync } from "node:fs";
@@ -88,6 +88,11 @@ create table if not exists a2_snapshot_pins (
88
88
  create index if not exists a2_snapshot_pins_checkpoint
89
89
  on a2_snapshot_pins (session_id, reducer_name, up_to_index);
90
90
 
91
+ create table if not exists a2_clears (
92
+ session_id text not null primary key,
93
+ clear_index integer not null
94
+ ) strict;
95
+
91
96
  create table if not exists a2_presence (
92
97
  ns text not null,
93
98
  participant text not null,
@@ -166,6 +171,7 @@ const toEvent = (row) => ({
166
171
  sessionId: row.session_id,
167
172
  createdAt: new Date(Number(row.created_at))
168
173
  });
174
+ const RECLAIM_BATCH = 256;
169
175
  function sqlite(options = {}) {
170
176
  const path = options.path ?? ".a2/dev.db";
171
177
  const clock = options.clock ?? SYSTEM_CLOCK;
@@ -188,6 +194,7 @@ function sqlite(options = {}) {
188
194
  when not exists (
189
195
  select 1 from a2_events
190
196
  where session_id = ? and lane = ? and processed_at is null
197
+ and idx > ?
191
198
  ) then 1
192
199
  else 0
193
200
  end,
@@ -197,6 +204,7 @@ function sqlite(options = {}) {
197
204
  and idx = (
198
205
  select idx from a2_events
199
206
  where session_id = ? and lane = ? and processed_at is null
207
+ and idx > ?
200
208
  order by idx limit 1
201
209
  )`);
202
210
  const appendStatus = db.prepare(`select coalesce(
@@ -205,7 +213,7 @@ function sqlite(options = {}) {
205
213
  ) as max,
206
214
  exists(
207
215
  select 1 from a2_events
208
- where session_id = ? and processed_at is null
216
+ where session_id = ? and processed_at is null and idx > ?
209
217
  ) as has_pending`);
210
218
  const upsertPresence = db.prepare(`insert into a2_presence (ns, participant, field, value, seen, at, expires_at)
211
219
  values (?, ?, ?, ?, ?, ?, ?)
@@ -227,10 +235,34 @@ function sqlite(options = {}) {
227
235
  select 1 from a2_events pending
228
236
  where pending.session_id = ?
229
237
  and pending.processed_at is null
238
+ and pending.idx > ?
230
239
  ) as session_has_pending
231
240
  from a2_events stored
232
241
  where stored.event_id in (${Array.from({ length: count }, () => "?").join(", ")})
233
242
  order by stored.idx`);
243
+ const selectClearIndex = db.prepare("select clear_index from a2_clears where session_id = ?");
244
+ const clearIndexOf = (sessionId) => Number(selectClearIndex.get(sessionId)?.clear_index ?? 0);
245
+ const deleteEventById = db.prepare("delete from a2_events where session_id = ? and event_id = ?");
246
+ /**
247
+ * The rows owning `ids`. This session's rows below its clear marker do
248
+ * not own their ids: they are deleted here so the ids can be reused.
249
+ */
250
+ const liveOwnersOf = (sessionId, clearIndex, ids) => {
251
+ if (ids.length === 0) return [];
252
+ return selectByIds(ids.length).all(sessionId, clearIndex, ...ids).filter((row) => {
253
+ if (row.session_id !== sessionId || Number(row.idx) >= clearIndex) return true;
254
+ deleteEventById.run(sessionId, row.event_id);
255
+ return false;
256
+ });
257
+ };
258
+ const selectCauseParent = db.prepare("select attempt_count, failed_at from a2_events where session_id = ? and idx = ?");
259
+ /** The attempt-currency fence shared by handler appends and clears. */
260
+ const assertCurrentCause = ({ sessionId, cause, clearIndex }) => {
261
+ if (cause.index <= clearIndex) throw supersededAttemptError(sessionId, cause);
262
+ const parent = selectCauseParent.get(sessionId, cause.index);
263
+ if (!parent) throw new TypeError(`no event at index ${cause.index} in session '${sessionId}'`);
264
+ if (Number(parent.attempt_count) !== cause.attempt || parent.failed_at !== null) throw supersededAttemptError(sessionId, cause);
265
+ };
234
266
  const tx = (fn) => {
235
267
  db.exec("begin immediate");
236
268
  try {
@@ -245,19 +277,20 @@ function sqlite(options = {}) {
245
277
  }
246
278
  };
247
279
  return {
248
- async append(sessionId, events) {
280
+ async append(sessionId, events, appendOptions) {
249
281
  if (events.length === 0) return wrap(() => {
250
- const status = appendStatus.get(sessionId, sessionId);
282
+ const status = appendStatus.get(sessionId, sessionId, clearIndexOf(sessionId));
251
283
  return {
252
284
  events: [],
253
285
  hasPending: Number(status.has_pending) === 1
254
286
  };
255
287
  });
256
288
  return wrap(() => tx(() => {
289
+ const clearIndex = clearIndexOf(sessionId);
257
290
  const suppliedIds = events.filter((e) => e.id !== void 0).map((e) => e.id);
258
291
  if (new Set(suppliedIds).size !== suppliedIds.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
259
292
  if (suppliedIds.length > 0) {
260
- const existing = selectByIds(suppliedIds.length).all(sessionId, ...suppliedIds);
293
+ const existing = liveOwnersOf(sessionId, clearIndex, suppliedIds);
261
294
  if (existing.length > 0) {
262
295
  const foreign = existing.find((row) => row.session_id !== sessionId);
263
296
  if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.event_id}' already exists in another session`);
@@ -268,20 +301,21 @@ function sqlite(options = {}) {
268
301
  throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
269
302
  }
270
303
  }
271
- for (const e of events) {
272
- if (!e.cause) continue;
273
- const parent = db.prepare("select attempt_count, failed_at from a2_events where session_id = ? and idx = ?").get(sessionId, e.cause.index);
274
- if (!parent) throw new TypeError(`no event at index ${e.cause.index} in session '${sessionId}'`);
275
- if (Number(parent.attempt_count) !== e.cause.attempt || parent.failed_at !== null) throw new A2Error("SUPERSEDED_ATTEMPT", `attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`);
276
- }
277
- const status = appendStatus.get(sessionId, sessionId);
304
+ const originIndex = appendOptions?.originIndex;
305
+ if (originIndex !== void 0 && originIndex <= clearIndex) throw sessionClearedError(`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`);
306
+ for (const e of events) if (e.cause) assertCurrentCause({
307
+ sessionId,
308
+ cause: e.cause,
309
+ clearIndex
310
+ });
311
+ const status = appendStatus.get(sessionId, sessionId, clearIndex);
278
312
  const base = Number(status.max);
279
313
  const now = clock.now().getTime();
280
314
  return {
281
315
  events: events.map((e, i) => {
282
316
  const id = e.id ?? generateId();
283
317
  const index = base + 1 + i;
284
- insertEvent.run(sessionId, index, e.type, JSON.stringify(e.payload) ?? "null", id, now, e.cause ? JSON.stringify(e.cause) : null, e.lane ?? null, e.lane ?? null, sessionId, e.lane ?? null, e.settled ? now : null);
318
+ insertEvent.run(sessionId, index, e.type, JSON.stringify(e.payload) ?? "null", id, now, e.cause ? JSON.stringify(e.cause) : null, e.lane ?? null, e.lane ?? null, sessionId, e.lane ?? null, clearIndex, e.settled ? now : null);
285
319
  return {
286
320
  id,
287
321
  type: e.type,
@@ -312,8 +346,8 @@ function sqlite(options = {}) {
312
346
  },
313
347
  async read(sessionId, opts) {
314
348
  return wrap(() => {
315
- const conditions = ["session_id = ?"];
316
- const params = [sessionId];
349
+ const conditions = ["session_id = ?", "idx >= coalesce((select clear_index from a2_clears where session_id = ?), 0)"];
350
+ const params = [sessionId, sessionId];
317
351
  if (opts?.afterIndex !== void 0) {
318
352
  conditions.push("idx > ?");
319
353
  params.push(opts.afterIndex);
@@ -329,6 +363,7 @@ function sqlite(options = {}) {
329
363
  return wrap(() => tx(() => {
330
364
  const now = clock.now().getTime();
331
365
  const expiresAt = expiresAtMs ?? now + ttlMs;
366
+ const clearIndex = clearIndexOf(sessionId);
332
367
  const exclusion = excludeIndexes.length === 0 ? "" : `and event.idx not in (${excludeIndexes.map(() => "?").join(", ")})`;
333
368
  const eligible = db.prepare(`select event.idx
334
369
  from a2_events event
@@ -336,9 +371,10 @@ function sqlite(options = {}) {
336
371
  and event.processed_at is null
337
372
  and event.failed_at is null
338
373
  and event.lane_ready = 1
374
+ and event.idx > ?
339
375
  and (event.claim_expires_at is null or event.claim_expires_at <= ?)
340
376
  ${exclusion}
341
- order by event.idx`).all(sessionId, now, ...excludeIndexes);
377
+ order by event.idx`).all(sessionId, clearIndex, now, ...excludeIndexes);
342
378
  if (eligible.length > 0) {
343
379
  const update = db.prepare(`update a2_events set
344
380
  attempt_count = attempt_count + 1,
@@ -352,7 +388,9 @@ function sqlite(options = {}) {
352
388
  outcome: "claimed",
353
389
  events: eligible.map((row) => {
354
390
  const claimed = update.get(now, now, holder, expiresAt, sessionId, row.idx);
355
- return toStored(claimed);
391
+ const stored = toStored(claimed);
392
+ if (clearIndex > 0) stored.clearIndex = clearIndex;
393
+ return stored;
356
394
  })
357
395
  };
358
396
  }
@@ -362,7 +400,8 @@ function sqlite(options = {}) {
362
400
  and processed_at is null
363
401
  and failed_at is null
364
402
  and lane_ready = 1
365
- and claim_expires_at > ?`).get(sessionId, now);
403
+ and idx > ?
404
+ and claim_expires_at > ?`).get(sessionId, clearIndex, now);
366
405
  return active.retry_at === null ? { outcome: "settled" } : {
367
406
  outcome: "busy",
368
407
  retryAt: new Date(Number(active.retry_at))
@@ -388,7 +427,12 @@ function sqlite(options = {}) {
388
427
  and failed_at is null
389
428
  and claim_holder = ?
390
429
  and claim_expires_at > ?`);
430
+ const clearIndex = clearIndexOf(sessionId);
391
431
  for (const claim of claims) {
432
+ if (claim.index <= clearIndex) {
433
+ superseded.push(claim.index);
434
+ continue;
435
+ }
392
436
  const row = currentAttempt.get(sessionId, claim.index);
393
437
  if (!row) continue;
394
438
  if (Number(row.attempt_count) > claim.attempt) {
@@ -406,6 +450,8 @@ function sqlite(options = {}) {
406
450
  },
407
451
  async completeAttempt({ sessionId, index, attempt, events }) {
408
452
  return wrap(() => tx(() => {
453
+ const clearIndex = clearIndexOf(sessionId);
454
+ if (index <= clearIndex) return { outcome: "superseded" };
409
455
  const parent = db.prepare("select * from a2_events where session_id = ? and idx = ?").get(sessionId, index);
410
456
  if (!parent) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
411
457
  const ids = events.map((event) => event.id);
@@ -414,7 +460,7 @@ function sqlite(options = {}) {
414
460
  if (Number(parent.processed_by_attempt) !== attempt) return { outcome: "superseded" };
415
461
  const returnedIds = parent.returned_event_ids === null ? null : decodeReturnedEventIds(parent.returned_event_ids);
416
462
  if (returnedIds === null || returnedIds.length !== ids.length || returnedIds.some((id, offset) => id !== ids[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
417
- const existing = ids.length === 0 ? [] : selectByIds(ids.length).all(sessionId, ...ids);
463
+ const existing = ids.length === 0 ? [] : selectByIds(ids.length).all(sessionId, clearIndex, ...ids);
418
464
  const byId = new Map(existing.map((row) => [row.event_id, row]));
419
465
  const ordered = ids.map((id) => byId.get(id));
420
466
  if (ordered.some((row) => !row || row.session_id !== sessionId || row.cause === null || toCause(row.cause)?.index !== index || toCause(row.cause)?.attempt !== attempt)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event retry does not match the committed batch");
@@ -424,8 +470,8 @@ function sqlite(options = {}) {
424
470
  };
425
471
  }
426
472
  if (Number(parent.attempt_count) !== attempt || parent.claim_holder === null || parent.failed_at !== null) return { outcome: "superseded" };
427
- if (ids.length > 0 && selectByIds(ids.length).all(sessionId, ...ids).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains an already-appended event id");
428
- const status = appendStatus.get(sessionId, sessionId);
473
+ if (liveOwnersOf(sessionId, clearIndex, ids).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains an already-appended event id");
474
+ const status = appendStatus.get(sessionId, sessionId, clearIndex);
429
475
  const now = clock.now().getTime();
430
476
  db.prepare(`update a2_events set
431
477
  processed_at = ?,
@@ -444,7 +490,7 @@ function sqlite(options = {}) {
444
490
  and pin.reducer_name = a2_snapshot_history.reducer_name
445
491
  and pin.up_to_index = a2_snapshot_history.up_to_index
446
492
  )`).run(sessionId);
447
- if (parent.lane !== null) promoteLaneHead.run(sessionId, sessionId, parent.lane);
493
+ if (parent.lane !== null) promoteLaneHead.run(sessionId, sessionId, parent.lane, clearIndex);
448
494
  return {
449
495
  outcome: "completed",
450
496
  events: events.map((event, offset) => {
@@ -454,7 +500,7 @@ function sqlite(options = {}) {
454
500
  index,
455
501
  attempt
456
502
  };
457
- insertEvent.run(sessionId, childIndex, event.type, JSON.stringify(event.payload) ?? "null", id, now, JSON.stringify(cause), event.lane ?? null, event.lane ?? null, sessionId, event.lane ?? null, event.settled ? now : null);
503
+ insertEvent.run(sessionId, childIndex, event.type, JSON.stringify(event.payload) ?? "null", id, now, JSON.stringify(cause), event.lane ?? null, event.lane ?? null, sessionId, event.lane ?? null, clearIndex, event.settled ? now : null);
458
504
  return toStored(db.prepare("select * from a2_events where session_id = ? and idx = ?").get(sessionId, childIndex));
459
505
  })
460
506
  };
@@ -462,6 +508,10 @@ function sqlite(options = {}) {
462
508
  },
463
509
  async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
464
510
  return wrap(() => tx(() => {
511
+ if (index <= clearIndexOf(sessionId)) return {
512
+ outcome: "superseded",
513
+ failureCount: 0
514
+ };
465
515
  const current = db.prepare("select * from a2_events where session_id = ? and idx = ?").get(sessionId, index);
466
516
  if (!current) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
467
517
  const failureCount = Number(current.failure_count);
@@ -514,10 +564,15 @@ function sqlite(options = {}) {
514
564
  return wrap(() => {
515
565
  const throughIndex = stateOptions?.throughIndex ?? null;
516
566
  const snapshotThroughIndex = stateOptions?.snapshotThroughIndex ?? throughIndex;
517
- const rows = db.prepare(`with head as materialized (
567
+ const rows = db.prepare(`with boundary as materialized (
568
+ select coalesce(
569
+ (select clear_index from a2_clears where session_id = ?), 0
570
+ ) as clear_index
571
+ ), head as materialized (
518
572
  select up_to_index, state, updated_at
519
573
  from a2_snapshots
520
574
  where session_id = ? and reducer_name = ?
575
+ and up_to_index >= (select clear_index from boundary)
521
576
  ), snapshot as materialized (
522
577
  select up_to_index, state from (
523
578
  select up_to_index, state from head
@@ -526,10 +581,12 @@ function sqlite(options = {}) {
526
581
  select up_to_index, state
527
582
  from a2_snapshot_history
528
583
  where session_id = ? and reducer_name = ?
584
+ and up_to_index >= (select clear_index from boundary)
529
585
  and (? is null or up_to_index <= ?)
530
586
  ) order by up_to_index desc limit 1
531
587
  )
532
588
  select 0 as row_order, 'snapshot' as row_kind,
589
+ (select clear_index from boundary) as clear_index,
533
590
  (select up_to_index from head) as head_index,
534
591
  (select up_to_index from snapshot) as snapshot_index,
535
592
  (select state from snapshot) as snapshot_state,
@@ -546,16 +603,22 @@ function sqlite(options = {}) {
546
603
  null as claim_expires_at
547
604
  union all
548
605
  select 1 as row_order, 'event' as row_kind,
606
+ null as clear_index,
549
607
  (select up_to_index from head) as head_index,
550
608
  null as snapshot_index, null as snapshot_state,
551
609
  event.*
552
610
  from a2_events as event
553
611
  where event.session_id = ?
554
- and event.idx > coalesce((select up_to_index from snapshot), 0)
612
+ and event.idx > max(
613
+ coalesce((select up_to_index from snapshot), 0),
614
+ (select clear_index from boundary) - 1
615
+ )
555
616
  and (? is null or event.idx <= ?)
556
- order by row_order, idx`).all(sessionId, reducerName, snapshotThroughIndex, snapshotThroughIndex, sessionId, reducerName, snapshotThroughIndex, snapshotThroughIndex, sessionId, throughIndex, throughIndex);
617
+ order by row_order, idx`).all(sessionId, sessionId, reducerName, snapshotThroughIndex, snapshotThroughIndex, sessionId, reducerName, snapshotThroughIndex, snapshotThroughIndex, sessionId, throughIndex, throughIndex);
557
618
  const snapshot = rows.find((row) => row.row_kind === "snapshot");
619
+ const clearIndex = Number(snapshot?.clear_index ?? 0);
558
620
  return {
621
+ ...clearIndex > 0 ? { clearIndex } : {},
559
622
  headIndex: snapshot?.head_index === null || snapshot?.head_index === void 0 ? null : Number(snapshot.head_index),
560
623
  snapshot: snapshot?.snapshot_index !== null && snapshot?.snapshot_index !== void 0 ? {
561
624
  index: Number(snapshot.snapshot_index),
@@ -570,7 +633,7 @@ function sqlite(options = {}) {
570
633
  const selectHead = db.prepare(`select up_to_index, state, updated_at from a2_snapshots
571
634
  where session_id = ? and reducer_name = ?`);
572
635
  const unfinished = db.prepare(`select 1 from a2_events
573
- where session_id = ? and idx = ?
636
+ where session_id = ? and idx = ? and idx > ?
574
637
  and processed_at is null and failed_at is null`);
575
638
  const insertPin = db.prepare(`insert or ignore into a2_snapshot_pins
576
639
  (session_id, event_index, reducer_name, up_to_index)
@@ -593,12 +656,14 @@ function sqlite(options = {}) {
593
656
  where excluded.up_to_index >= a2_snapshots.up_to_index`);
594
657
  const deleteHistory = db.prepare(`delete from a2_snapshot_history
595
658
  where session_id = ? and reducer_name = ? and up_to_index = ?`);
659
+ const clearIndex = clearIndexOf(sessionId);
596
660
  for (const write of writes.toSorted((a, b) => a.index - b.index)) {
597
- for (const eventIndex of write.pinEventIndexes ?? []) if (unfinished.get(sessionId, eventIndex)) insertPin.run(sessionId, eventIndex, reducerName, write.index);
661
+ if (write.index < clearIndex) continue;
662
+ for (const eventIndex of write.pinEventIndexes ?? []) if (unfinished.get(sessionId, eventIndex, clearIndex)) insertPin.run(sessionId, eventIndex, reducerName, write.index);
598
663
  const head = selectHead.get(sessionId, reducerName);
599
664
  const now = clock.now().getTime();
600
665
  if (!head || write.index >= Number(head.up_to_index)) {
601
- if (head && write.index > Number(head.up_to_index) && hasPin.get(sessionId, reducerName, Number(head.up_to_index))) putHistory.run(sessionId, reducerName, Number(head.up_to_index), head.state, Number(head.updated_at));
666
+ if (head && write.index > Number(head.up_to_index) && Number(head.up_to_index) >= clearIndex && hasPin.get(sessionId, reducerName, Number(head.up_to_index))) putHistory.run(sessionId, reducerName, Number(head.up_to_index), head.state, Number(head.updated_at));
602
667
  putHead.run(sessionId, reducerName, write.index, JSON.stringify(write.state) ?? "null", now);
603
668
  deleteHistory.run(sessionId, reducerName, write.index);
604
669
  } else if (write.index < Number(head.up_to_index) && hasPin.get(sessionId, reducerName, write.index)) putHistory.run(sessionId, reducerName, write.index, JSON.stringify(write.state) ?? "null", now);
@@ -633,10 +698,79 @@ function sqlite(options = {}) {
633
698
  },
634
699
  stream(sessionId, opts) {
635
700
  const readAfter = (afterIndex) => wrap(() => {
636
- return db.prepare("select * from a2_events where session_id = ? and idx > ? order by idx").all(sessionId, afterIndex).map(toStored);
701
+ return db.prepare(`select * from a2_events
702
+ where session_id = ? and idx > ?
703
+ and idx >= coalesce((select clear_index from a2_clears where session_id = ?), 0)
704
+ order by idx`).all(sessionId, afterIndex, sessionId).map(toStored);
637
705
  });
638
706
  return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
639
707
  },
708
+ async clear({ sessionId, ifLastIndex, cause }) {
709
+ return wrap(() => tx(() => {
710
+ const clearIndex = clearIndexOf(sessionId);
711
+ if (cause) assertCurrentCause({
712
+ sessionId,
713
+ cause,
714
+ clearIndex
715
+ });
716
+ const head = Number(appendStatus.get(sessionId, sessionId, clearIndex).max);
717
+ if (ifLastIndex !== void 0 && clearIndex === ifLastIndex + 1) return {
718
+ cleared: true,
719
+ index: head,
720
+ clearIndex
721
+ };
722
+ if (ifLastIndex !== void 0 && ifLastIndex !== head) return {
723
+ cleared: false,
724
+ index: head,
725
+ clearIndex
726
+ };
727
+ if (head === 0 || clearIndex === head) return {
728
+ cleared: true,
729
+ index: head,
730
+ clearIndex
731
+ };
732
+ const marker = head + 1;
733
+ const now = clock.now().getTime();
734
+ insertEvent.run(sessionId, marker, CLEAR_EVENT_TYPE, "null", generateId(), now, null, null, null, sessionId, null, clearIndex, now);
735
+ db.prepare(`insert into a2_clears (session_id, clear_index) values (?, ?)
736
+ on conflict (session_id) do update set clear_index = excluded.clear_index`).run(sessionId, marker);
737
+ return {
738
+ cleared: true,
739
+ index: marker,
740
+ clearIndex: marker
741
+ };
742
+ }));
743
+ },
744
+ async reclaim(sessionId) {
745
+ return wrap(() => tx(() => {
746
+ const clearIndex = clearIndexOf(sessionId);
747
+ if (clearIndex === 0) return {
748
+ deleted: 0,
749
+ done: true
750
+ };
751
+ const last = db.prepare(`select max(idx) as last from (
752
+ select idx from a2_events
753
+ where session_id = ? and idx < ?
754
+ order by idx limit ?
755
+ )`).get(sessionId, clearIndex, RECLAIM_BATCH);
756
+ let deleted = 0;
757
+ if (last.last !== null) {
758
+ const through = Number(last.last);
759
+ deleted = Number(db.prepare("delete from a2_events where session_id = ? and idx <= ?").run(sessionId, through).changes);
760
+ db.prepare("delete from a2_snapshot_pins where session_id = ? and event_index <= ?").run(sessionId, through);
761
+ }
762
+ const done = db.prepare("select 1 from a2_events where session_id = ? and idx < ? limit 1").get(sessionId, clearIndex) === void 0;
763
+ if (done) for (const table of [
764
+ "a2_snapshot_pins",
765
+ "a2_snapshot_history",
766
+ "a2_snapshots"
767
+ ]) db.prepare(`delete from ${table} where session_id = ? and up_to_index < ?`).run(sessionId, clearIndex);
768
+ return {
769
+ deleted,
770
+ done
771
+ };
772
+ }));
773
+ },
640
774
  close() {
641
775
  db.close();
642
776
  }