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
@@ -15,7 +15,8 @@
15
15
  import { mkdirSync } from 'node:fs'
16
16
  import { dirname, resolve } from 'node:path'
17
17
  import { DatabaseSync } from 'node:sqlite'
18
- import { A2Error } from './errors.ts'
18
+ import { A2Error, supersededAttemptError } from './errors.ts'
19
+ import { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'
19
20
  import { idempotentReplay } from './idempotent-replay.ts'
20
21
  import { decodeReturnedEventIds } from './store-codec.ts'
21
22
  import { pollingStream } from './store-polling.ts'
@@ -112,6 +113,11 @@ create table if not exists a2_snapshot_pins (
112
113
  create index if not exists a2_snapshot_pins_checkpoint
113
114
  on a2_snapshot_pins (session_id, reducer_name, up_to_index);
114
115
 
116
+ create table if not exists a2_clears (
117
+ session_id text not null primary key,
118
+ clear_index integer not null
119
+ ) strict;
120
+
115
121
  create table if not exists a2_presence (
116
122
  ns text not null,
117
123
  participant text not null,
@@ -262,6 +268,7 @@ type PresenceDbRow = {
262
268
 
263
269
  type StateReadRow = Partial<EventRow> & {
264
270
  row_kind: 'snapshot' | 'event'
271
+ clear_index: number | bigint | null
265
272
  head_index: number | bigint | null
266
273
  snapshot_index: number | bigint | null
267
274
  snapshot_state: string | null
@@ -282,6 +289,8 @@ type ExistingEventRow = EventRow & {
282
289
  session_has_pending: number | bigint
283
290
  }
284
291
 
292
+ const RECLAIM_BATCH = 256
293
+
285
294
  export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
286
295
  const path = options.path ?? '.a2/dev.db'
287
296
  const clock = options.clock ?? SYSTEM_CLOCK
@@ -314,6 +323,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
314
323
  when not exists (
315
324
  select 1 from a2_events
316
325
  where session_id = ? and lane = ? and processed_at is null
326
+ and idx > ?
317
327
  ) then 1
318
328
  else 0
319
329
  end,
@@ -325,6 +335,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
325
335
  and idx = (
326
336
  select idx from a2_events
327
337
  where session_id = ? and lane = ? and processed_at is null
338
+ and idx > ?
328
339
  order by idx limit 1
329
340
  )`,
330
341
  )
@@ -335,7 +346,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
335
346
  ) as max,
336
347
  exists(
337
348
  select 1 from a2_events
338
- where session_id = ? and processed_at is null
349
+ where session_id = ? and processed_at is null and idx > ?
339
350
  ) as has_pending`,
340
351
  )
341
352
  const upsertPresence = db.prepare(
@@ -368,12 +379,80 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
368
379
  select 1 from a2_events pending
369
380
  where pending.session_id = ?
370
381
  and pending.processed_at is null
382
+ and pending.idx > ?
371
383
  ) as session_has_pending
372
384
  from a2_events stored
373
385
  where stored.event_id in (${Array.from({ length: count }, () => '?').join(', ')})
374
386
  order by stored.idx`,
375
387
  )
376
388
 
389
+ const selectClearIndex = db.prepare(
390
+ 'select clear_index from a2_clears where session_id = ?',
391
+ )
392
+ const clearIndexOf = (sessionId: string): number =>
393
+ Number(
394
+ (
395
+ selectClearIndex.get(sessionId) as
396
+ { clear_index: number | bigint } | undefined
397
+ )?.clear_index ?? 0,
398
+ )
399
+ const deleteEventById = db.prepare(
400
+ 'delete from a2_events where session_id = ? and event_id = ?',
401
+ )
402
+
403
+ /**
404
+ * The rows owning `ids`. This session's rows below its clear marker do
405
+ * not own their ids: they are deleted here so the ids can be reused.
406
+ */
407
+ const liveOwnersOf = (
408
+ sessionId: string,
409
+ clearIndex: number,
410
+ ids: readonly string[],
411
+ ): ExistingEventRow[] => {
412
+ if (ids.length === 0) return []
413
+ const rows = selectByIds(ids.length).all(
414
+ sessionId,
415
+ clearIndex,
416
+ ...ids,
417
+ ) as unknown as ExistingEventRow[]
418
+ return rows.filter((row) => {
419
+ if (row.session_id !== sessionId || Number(row.idx) >= clearIndex)
420
+ return true
421
+ deleteEventById.run(sessionId, row.event_id)
422
+ return false
423
+ })
424
+ }
425
+
426
+ const selectCauseParent = db.prepare(
427
+ 'select attempt_count, failed_at from a2_events where session_id = ? and idx = ?',
428
+ )
429
+ /** The attempt-currency fence shared by handler appends and clears. */
430
+ const assertCurrentCause = ({
431
+ sessionId,
432
+ cause,
433
+ clearIndex,
434
+ }: {
435
+ sessionId: string
436
+ cause: EventCause
437
+ clearIndex: number
438
+ }): void => {
439
+ if (cause.index <= clearIndex)
440
+ throw supersededAttemptError(sessionId, cause)
441
+ const parent = selectCauseParent.get(sessionId, cause.index) as
442
+ { attempt_count: number | bigint; failed_at: unknown } | undefined
443
+ if (!parent) {
444
+ throw new TypeError(
445
+ `no event at index ${cause.index} in session '${sessionId}'`,
446
+ )
447
+ }
448
+ if (
449
+ Number(parent.attempt_count) !== cause.attempt ||
450
+ parent.failed_at !== null
451
+ ) {
452
+ throw supersededAttemptError(sessionId, cause)
453
+ }
454
+ }
455
+
377
456
  const tx = <T>(fn: () => T): T => {
378
457
  db.exec('begin immediate')
379
458
  try {
@@ -392,12 +471,13 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
392
471
  }
393
472
 
394
473
  return {
395
- async append(sessionId, events) {
474
+ async append(sessionId, events, appendOptions) {
396
475
  if (events.length === 0) {
397
476
  return wrap(() => {
398
477
  const status = appendStatus.get(
399
478
  sessionId,
400
479
  sessionId,
480
+ clearIndexOf(sessionId),
401
481
  ) as AppendStatusRow
402
482
  return {
403
483
  events: [],
@@ -407,6 +487,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
407
487
  }
408
488
  return wrap(() =>
409
489
  tx(() => {
490
+ const clearIndex = clearIndexOf(sessionId)
410
491
  const supplied = events.filter((e) => e.id !== undefined)
411
492
  const suppliedIds = supplied.map((e) => e.id as string)
412
493
  if (new Set(suppliedIds).size !== suppliedIds.length) {
@@ -416,10 +497,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
416
497
  )
417
498
  }
418
499
  if (suppliedIds.length > 0) {
419
- const existing = selectByIds(suppliedIds.length).all(
420
- sessionId,
421
- ...suppliedIds,
422
- ) as unknown as ExistingEventRow[]
500
+ const existing = liveOwnersOf(sessionId, clearIndex, suppliedIds)
423
501
  if (existing.length > 0) {
424
502
  const foreign = existing.find(
425
503
  (row) => row.session_id !== sessionId,
@@ -443,37 +521,25 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
443
521
  }
444
522
  }
445
523
 
524
+ const originIndex = appendOptions?.originIndex
525
+ if (originIndex !== undefined && originIndex <= clearIndex) {
526
+ throw sessionClearedError(
527
+ `scheduled work from event ${originIndex} was cleared in session '${sessionId}'`,
528
+ )
529
+ }
446
530
  // Attempt-currency fence: a fresh handler append commits only
447
531
  // while its causal attempt is still the parent's latest
448
532
  // (store.ts `append`). `begin immediate` serializes this read
449
533
  // against a concurrent claim.
450
534
  for (const e of events) {
451
- if (!e.cause) continue
452
- const parent = db
453
- .prepare(
454
- 'select attempt_count, failed_at from a2_events where session_id = ? and idx = ?',
455
- )
456
- .get(sessionId, e.cause.index) as
457
- { attempt_count: number | bigint; failed_at: unknown } | undefined
458
- if (!parent) {
459
- throw new TypeError(
460
- `no event at index ${e.cause.index} in session '${sessionId}'`,
461
- )
462
- }
463
- if (
464
- Number(parent.attempt_count) !== e.cause.attempt ||
465
- parent.failed_at !== null
466
- ) {
467
- throw new A2Error(
468
- 'SUPERSEDED_ATTEMPT',
469
- `attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`,
470
- )
471
- }
535
+ if (e.cause)
536
+ assertCurrentCause({ sessionId, cause: e.cause, clearIndex })
472
537
  }
473
538
 
474
539
  const status = appendStatus.get(
475
540
  sessionId,
476
541
  sessionId,
542
+ clearIndex,
477
543
  ) as AppendStatusRow
478
544
  const base = Number(status.max)
479
545
  const now = clock.now().getTime()
@@ -492,6 +558,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
492
558
  e.lane ?? null,
493
559
  sessionId,
494
560
  e.lane ?? null,
561
+ clearIndex,
495
562
  e.settled ? now : null,
496
563
  )
497
564
  return {
@@ -530,8 +597,12 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
530
597
 
531
598
  async read(sessionId, opts) {
532
599
  return wrap(() => {
533
- const conditions = ['session_id = ?']
534
- const params: (string | number)[] = [sessionId]
600
+ // Reads start at the latest clear marker.
601
+ const conditions = [
602
+ 'session_id = ?',
603
+ 'idx >= coalesce((select clear_index from a2_clears where session_id = ?), 0)',
604
+ ]
605
+ const params: (string | number)[] = [sessionId, sessionId]
535
606
  if (opts?.afterIndex !== undefined) {
536
607
  conditions.push('idx > ?')
537
608
  params.push(opts.afterIndex)
@@ -560,6 +631,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
560
631
  tx(() => {
561
632
  const now = clock.now().getTime()
562
633
  const expiresAt = expiresAtMs ?? now + ttlMs
634
+ const clearIndex = clearIndexOf(sessionId)
563
635
  const exclusion =
564
636
  excludeIndexes.length === 0
565
637
  ? ''
@@ -572,11 +644,17 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
572
644
  and event.processed_at is null
573
645
  and event.failed_at is null
574
646
  and event.lane_ready = 1
647
+ and event.idx > ?
575
648
  and (event.claim_expires_at is null or event.claim_expires_at <= ?)
576
649
  ${exclusion}
577
650
  order by event.idx`,
578
651
  )
579
- .all(sessionId, now, ...excludeIndexes) as unknown as Array<{
652
+ .all(
653
+ sessionId,
654
+ clearIndex,
655
+ now,
656
+ ...excludeIndexes,
657
+ ) as unknown as Array<{
580
658
  idx: number | bigint
581
659
  }>
582
660
  if (eligible.length > 0) {
@@ -599,7 +677,9 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
599
677
  sessionId,
600
678
  row.idx,
601
679
  ) as EventRow
602
- return toStored(claimed)
680
+ const stored = toStored(claimed)
681
+ if (clearIndex > 0) stored.clearIndex = clearIndex
682
+ return stored
603
683
  })
604
684
  return { outcome: 'claimed', events }
605
685
  }
@@ -612,9 +692,10 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
612
692
  and processed_at is null
613
693
  and failed_at is null
614
694
  and lane_ready = 1
695
+ and idx > ?
615
696
  and claim_expires_at > ?`,
616
697
  )
617
- .get(sessionId, now) as {
698
+ .get(sessionId, clearIndex, now) as {
618
699
  retry_at: number | bigint | null
619
700
  }
620
701
  return active.retry_at === null
@@ -645,7 +726,12 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
645
726
  and claim_holder = ?
646
727
  and claim_expires_at > ?`,
647
728
  )
729
+ const clearIndex = clearIndexOf(sessionId)
648
730
  for (const claim of claims) {
731
+ if (claim.index <= clearIndex) {
732
+ superseded.push(claim.index)
733
+ continue
734
+ }
649
735
  const row = currentAttempt.get(sessionId, claim.index) as
650
736
  { attempt_count: number | bigint } | undefined
651
737
  if (!row) continue
@@ -674,6 +760,8 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
674
760
  async completeAttempt({ sessionId, index, attempt, events }) {
675
761
  return wrap(() =>
676
762
  tx(() => {
763
+ const clearIndex = clearIndexOf(sessionId)
764
+ if (index <= clearIndex) return { outcome: 'superseded' }
677
765
  const parent = db
678
766
  .prepare('select * from a2_events where session_id = ? and idx = ?')
679
767
  .get(sessionId, index) as EventRow | undefined
@@ -713,6 +801,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
713
801
  ? []
714
802
  : (selectByIds(ids.length).all(
715
803
  sessionId,
804
+ clearIndex,
716
805
  ...ids,
717
806
  ) as unknown as ExistingEventRow[])
718
807
  const byId = new Map(existing.map((row) => [row.event_id, row]))
@@ -748,11 +837,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
748
837
  return { outcome: 'superseded' }
749
838
  }
750
839
 
751
- if (
752
- ids.length > 0 &&
753
- (selectByIds(ids.length).all(sessionId, ...ids) as unknown[])
754
- .length > 0
755
- ) {
840
+ if (liveOwnersOf(sessionId, clearIndex, ids).length > 0) {
756
841
  throw new A2Error(
757
842
  'PARTIAL_DUPLICATE_BATCH',
758
843
  'returned event batch contains an already-appended event id',
@@ -762,6 +847,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
762
847
  const status = appendStatus.get(
763
848
  sessionId,
764
849
  sessionId,
850
+ clearIndex,
765
851
  ) as AppendStatusRow
766
852
  const now = clock.now().getTime()
767
853
  db.prepare(
@@ -788,7 +874,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
788
874
  )`,
789
875
  ).run(sessionId)
790
876
  if (parent.lane !== null) {
791
- promoteLaneHead.run(sessionId, sessionId, parent.lane)
877
+ promoteLaneHead.run(sessionId, sessionId, parent.lane, clearIndex)
792
878
  }
793
879
  const inserted = events.map((event, offset) => {
794
880
  const id = event.id
@@ -806,6 +892,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
806
892
  event.lane ?? null,
807
893
  sessionId,
808
894
  event.lane ?? null,
895
+ clearIndex,
809
896
  event.settled ? now : null,
810
897
  )
811
898
  return toStored(
@@ -824,6 +911,9 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
824
911
  async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
825
912
  return wrap(() =>
826
913
  tx(() => {
914
+ if (index <= clearIndexOf(sessionId)) {
915
+ return { outcome: 'superseded', failureCount: 0 }
916
+ }
827
917
  const current = db
828
918
  .prepare('select * from a2_events where session_id = ? and idx = ?')
829
919
  .get(sessionId, index) as EventRow | undefined
@@ -905,10 +995,15 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
905
995
  stateOptions?.snapshotThroughIndex ?? throughIndex
906
996
  const rows = db
907
997
  .prepare(
908
- `with head as materialized (
998
+ `with boundary as materialized (
999
+ select coalesce(
1000
+ (select clear_index from a2_clears where session_id = ?), 0
1001
+ ) as clear_index
1002
+ ), head as materialized (
909
1003
  select up_to_index, state, updated_at
910
1004
  from a2_snapshots
911
1005
  where session_id = ? and reducer_name = ?
1006
+ and up_to_index >= (select clear_index from boundary)
912
1007
  ), snapshot as materialized (
913
1008
  select up_to_index, state from (
914
1009
  select up_to_index, state from head
@@ -917,10 +1012,12 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
917
1012
  select up_to_index, state
918
1013
  from a2_snapshot_history
919
1014
  where session_id = ? and reducer_name = ?
1015
+ and up_to_index >= (select clear_index from boundary)
920
1016
  and (? is null or up_to_index <= ?)
921
1017
  ) order by up_to_index desc limit 1
922
1018
  )
923
1019
  select 0 as row_order, 'snapshot' as row_kind,
1020
+ (select clear_index from boundary) as clear_index,
924
1021
  (select up_to_index from head) as head_index,
925
1022
  (select up_to_index from snapshot) as snapshot_index,
926
1023
  (select state from snapshot) as snapshot_state,
@@ -937,16 +1034,21 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
937
1034
  null as claim_expires_at
938
1035
  union all
939
1036
  select 1 as row_order, 'event' as row_kind,
1037
+ null as clear_index,
940
1038
  (select up_to_index from head) as head_index,
941
1039
  null as snapshot_index, null as snapshot_state,
942
1040
  event.*
943
1041
  from a2_events as event
944
1042
  where event.session_id = ?
945
- and event.idx > coalesce((select up_to_index from snapshot), 0)
1043
+ and event.idx > max(
1044
+ coalesce((select up_to_index from snapshot), 0),
1045
+ (select clear_index from boundary) - 1
1046
+ )
946
1047
  and (? is null or event.idx <= ?)
947
1048
  order by row_order, idx`,
948
1049
  )
949
1050
  .all(
1051
+ sessionId,
950
1052
  sessionId,
951
1053
  reducerName,
952
1054
  snapshotThroughIndex,
@@ -960,7 +1062,9 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
960
1062
  throughIndex,
961
1063
  ) as unknown as StateReadRow[]
962
1064
  const snapshot = rows.find((row) => row.row_kind === 'snapshot')
1065
+ const clearIndex = Number(snapshot?.clear_index ?? 0)
963
1066
  return {
1067
+ ...(clearIndex > 0 ? { clearIndex } : {}),
964
1068
  headIndex:
965
1069
  snapshot?.head_index === null || snapshot?.head_index === undefined
966
1070
  ? null
@@ -991,7 +1095,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
991
1095
  )
992
1096
  const unfinished = db.prepare(
993
1097
  `select 1 from a2_events
994
- where session_id = ? and idx = ?
1098
+ where session_id = ? and idx = ? and idx > ?
995
1099
  and processed_at is null and failed_at is null`,
996
1100
  )
997
1101
  const insertPin = db.prepare(
@@ -1025,9 +1129,12 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
1025
1129
  `delete from a2_snapshot_history
1026
1130
  where session_id = ? and reducer_name = ? and up_to_index = ?`,
1027
1131
  )
1132
+ const clearIndex = clearIndexOf(sessionId)
1028
1133
  for (const write of writes.toSorted((a, b) => a.index - b.index)) {
1134
+ // A fold from before the latest clear describes a lifetime that is gone.
1135
+ if (write.index < clearIndex) continue
1029
1136
  for (const eventIndex of write.pinEventIndexes ?? []) {
1030
- if (unfinished.get(sessionId, eventIndex)) {
1137
+ if (unfinished.get(sessionId, eventIndex, clearIndex)) {
1031
1138
  insertPin.run(sessionId, eventIndex, reducerName, write.index)
1032
1139
  }
1033
1140
  }
@@ -1038,6 +1145,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
1038
1145
  if (
1039
1146
  head &&
1040
1147
  write.index > Number(head.up_to_index) &&
1148
+ Number(head.up_to_index) >= clearIndex &&
1041
1149
  hasPin.get(sessionId, reducerName, Number(head.up_to_index))
1042
1150
  ) {
1043
1151
  putHistory.run(
@@ -1126,11 +1234,15 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
1126
1234
  stream(sessionId, opts) {
1127
1235
  const readAfter = (afterIndex: number): StoredEvent[] =>
1128
1236
  wrap(() => {
1237
+ // A cursor below the latest marker resumes at it.
1129
1238
  const rows = db
1130
1239
  .prepare(
1131
- 'select * from a2_events where session_id = ? and idx > ? order by idx',
1240
+ `select * from a2_events
1241
+ where session_id = ? and idx > ?
1242
+ and idx >= coalesce((select clear_index from a2_clears where session_id = ?), 0)
1243
+ order by idx`,
1132
1244
  )
1133
- .all(sessionId, afterIndex) as unknown as EventRow[]
1245
+ .all(sessionId, afterIndex, sessionId) as unknown as EventRow[]
1134
1246
  return rows.map(toStored)
1135
1247
  })
1136
1248
  return pollingStream(
@@ -1139,6 +1251,108 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
1139
1251
  )
1140
1252
  },
1141
1253
 
1254
+ async clear({ sessionId, ifLastIndex, cause }) {
1255
+ return wrap(() =>
1256
+ tx(() => {
1257
+ const clearIndex = clearIndexOf(sessionId)
1258
+ if (cause) assertCurrentCause({ sessionId, cause, clearIndex })
1259
+ const head = Number(
1260
+ (
1261
+ appendStatus.get(
1262
+ sessionId,
1263
+ sessionId,
1264
+ clearIndex,
1265
+ ) as AppendStatusRow
1266
+ ).max,
1267
+ )
1268
+ if (ifLastIndex !== undefined && clearIndex === ifLastIndex + 1) {
1269
+ return { cleared: true, index: head, clearIndex }
1270
+ }
1271
+ if (ifLastIndex !== undefined && ifLastIndex !== head) {
1272
+ return { cleared: false, index: head, clearIndex }
1273
+ }
1274
+ if (head === 0 || clearIndex === head) {
1275
+ return { cleared: true, index: head, clearIndex }
1276
+ }
1277
+ const marker = head + 1
1278
+ const now = clock.now().getTime()
1279
+ insertEvent.run(
1280
+ sessionId,
1281
+ marker,
1282
+ CLEAR_EVENT_TYPE,
1283
+ 'null',
1284
+ generateId(),
1285
+ now,
1286
+ null,
1287
+ null,
1288
+ null,
1289
+ sessionId,
1290
+ null,
1291
+ clearIndex,
1292
+ now,
1293
+ )
1294
+ db.prepare(
1295
+ `insert into a2_clears (session_id, clear_index) values (?, ?)
1296
+ on conflict (session_id) do update set clear_index = excluded.clear_index`,
1297
+ ).run(sessionId, marker)
1298
+ return { cleared: true, index: marker, clearIndex: marker }
1299
+ }),
1300
+ )
1301
+ },
1302
+
1303
+ async reclaim(sessionId) {
1304
+ return wrap(() =>
1305
+ tx(() => {
1306
+ const clearIndex = clearIndexOf(sessionId)
1307
+ if (clearIndex === 0) return { deleted: 0, done: true }
1308
+ const last = db
1309
+ .prepare(
1310
+ `select max(idx) as last from (
1311
+ select idx from a2_events
1312
+ where session_id = ? and idx < ?
1313
+ order by idx limit ?
1314
+ )`,
1315
+ )
1316
+ .get(sessionId, clearIndex, RECLAIM_BATCH) as {
1317
+ last: number | bigint | null
1318
+ }
1319
+ let deleted = 0
1320
+ if (last.last !== null) {
1321
+ const through = Number(last.last)
1322
+ deleted = Number(
1323
+ db
1324
+ .prepare(
1325
+ 'delete from a2_events where session_id = ? and idx <= ?',
1326
+ )
1327
+ .run(sessionId, through).changes,
1328
+ )
1329
+ db.prepare(
1330
+ 'delete from a2_snapshot_pins where session_id = ? and event_index <= ?',
1331
+ ).run(sessionId, through)
1332
+ }
1333
+ const done =
1334
+ db
1335
+ .prepare(
1336
+ 'select 1 from a2_events where session_id = ? and idx < ? limit 1',
1337
+ )
1338
+ .get(sessionId, clearIndex) === undefined
1339
+ // Writes serialize with the clear, so none lands below the marker later.
1340
+ if (done) {
1341
+ for (const table of [
1342
+ 'a2_snapshot_pins',
1343
+ 'a2_snapshot_history',
1344
+ 'a2_snapshots',
1345
+ ]) {
1346
+ db.prepare(
1347
+ `delete from ${table} where session_id = ? and up_to_index < ?`,
1348
+ ).run(sessionId, clearIndex)
1349
+ }
1350
+ }
1351
+ return { deleted, done }
1352
+ }),
1353
+ )
1354
+ },
1355
+
1142
1356
  close() {
1143
1357
  db.close()
1144
1358
  },
package/src/store.ts CHANGED
@@ -65,6 +65,8 @@ export type StoredEvent = Event & {
65
65
  lastError: string | null
66
66
  /** Adapter clock time recorded when dead-lettered; null otherwise. */
67
67
  failedAt: Date | null
68
+ /** Latest clear marker, on claimed events of a cleared session. */
69
+ clearIndex?: number
68
70
  }
69
71
 
70
72
  /**
@@ -91,6 +93,8 @@ export type StoreStateRead = {
91
93
  snapshot: { index: number; state: unknown } | null
92
94
  /** Immutable events strictly after `snapshot.index`, or the full log on a miss. */
93
95
  events: Event[]
96
+ /** Latest clear marker; omitted when the session was never cleared. */
97
+ clearIndex?: number
94
98
  }
95
99
 
96
100
  export type StoreSnapshotWrite = {
@@ -153,6 +157,23 @@ export type StoreAppendResult = {
153
157
  hasPending: boolean
154
158
  }
155
159
 
160
+ export type StoreClearOptions = {
161
+ sessionId: string
162
+ ifLastIndex?: number
163
+ /** The clearing handler's dispatch, fenced like a handler append. */
164
+ cause?: EventCause
165
+ }
166
+
167
+ export type StoreClearResult = {
168
+ cleared: boolean
169
+ /** The head after the operation. */
170
+ index: number
171
+ /** The latest clear marker after the operation, or 0. */
172
+ clearIndex: number
173
+ }
174
+
175
+ export type StoreReclaimResult = { deleted: number; done: boolean }
176
+
156
177
  /**
157
178
  * An injectable clock. Adapters take one so tests can drive claim
158
179
  * expiry, failure timestamps, and (later) stuck-session detection
@@ -193,8 +214,32 @@ export interface A2Store {
193
214
  * `claimAvailable` — an unlocked read of the parent admits write skew.
194
215
  * The idempotent-replay path runs first, so a batch whose ids all exist
195
216
  * replays regardless of the current attempt.
217
+ *
218
+ * Clearing stores (a2-implementation.md §17.2) fence one level earlier:
219
+ * a `cause` at or below the latest clear marker throws
220
+ * `SUPERSEDED_ATTEMPT`, an `originIndex` there throws `SESSION_CLEARED`,
221
+ * and ids whose rows lie below the marker are free to reuse.
222
+ */
223
+ append(
224
+ sessionId: string,
225
+ events: AppendEvent[],
226
+ options?: { originIndex?: number },
227
+ ): Promise<StoreAppendResult>
228
+
229
+ /**
230
+ * Optional with `reclaim`. Serialized with `append`: checks
231
+ * `ifLastIndex` against the head and the latest marker, then appends a
232
+ * settled clear marker at the next index without touching existing
233
+ * rows. Afterwards every event at or below the marker is superseded.
234
+ */
235
+ clear?(options: StoreClearOptions): Promise<StoreClearResult>
236
+
237
+ /**
238
+ * Deletes one bounded batch of rows below the latest clear marker, with
239
+ * their bookkeeping, id records, and cached folds. `done` once nothing
240
+ * remains below it. Never waits on a lock that other operations hold.
196
241
  */
197
- append(sessionId: string, events: AppendEvent[]): Promise<StoreAppendResult>
242
+ reclaim?(sessionId: string): Promise<StoreReclaimResult>
198
243
 
199
244
  /** Events for one session, oldest first. Bounds form `(afterIndex, throughIndex]`. */
200
245
  read(