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
@@ -26,10 +26,27 @@ export type SchedulerAppendTask = {
26
26
  /** Earliest useful delivery as epoch milliseconds. */
27
27
  dueAt: number
28
28
  events: readonly ScheduledEvent[]
29
+ /** The scheduling handler's event; delivery skips once it is cleared. */
30
+ originIndex?: number
31
+ }
32
+
33
+ /** A durable conditional clear. */
34
+ export type SchedulerClearTask = {
35
+ version: 1
36
+ kind: 'clear'
37
+ /** Stable identity for provider-level deduplication. */
38
+ id: string
39
+ contract: string
40
+ sessionId: string
41
+ /** Earliest useful delivery as epoch milliseconds. */
42
+ dueAt: number
43
+ /** Clear only while this is still the head. */
44
+ ifLastIndex: number
29
45
  }
30
46
 
31
47
  /** The versioned message protocol shared by scheduler adapters. */
32
- export type SchedulerTask = SchedulerDrainTask | SchedulerAppendTask
48
+ export type SchedulerTask =
49
+ SchedulerDrainTask | SchedulerAppendTask | SchedulerClearTask
33
50
 
34
51
  const recordOf = (value: unknown): Record<string, unknown> | null =>
35
52
  value !== null && typeof value === 'object' && !Array.isArray(value)
@@ -42,6 +59,9 @@ const validName = (value: unknown): value is string =>
42
59
  const validDueAt = (value: unknown): value is number =>
43
60
  typeof value === 'number' && Number.isInteger(value) && value % 1_000 === 0
44
61
 
62
+ const validIndex = (value: unknown): value is number =>
63
+ typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
64
+
45
65
  const baseTask = (
46
66
  record: Record<string, unknown>,
47
67
  ): { contract: string; sessionId: string } | null =>
@@ -68,10 +88,21 @@ export function normalizeSchedulerTask(value: unknown): SchedulerTask | null {
68
88
  if (!validDueAt(dueAt)) return null
69
89
  return { version: 1, kind: 'drain', ...base, dueAt }
70
90
  }
91
+ if (record['kind'] === 'clear') {
92
+ const { dueAt, id, ifLastIndex } = record
93
+ if (!validName(id) || !validDueAt(dueAt) || !validIndex(ifLastIndex))
94
+ return null
95
+ return { version: 1, kind: 'clear', id, ...base, dueAt, ifLastIndex }
96
+ }
71
97
  if (record['kind'] !== 'append') return null
72
98
 
73
- const { dueAt, events, id } = record
99
+ const { dueAt, events, id, originIndex } = record
74
100
  if (!validName(id) || !validDueAt(dueAt)) return null
101
+ if (
102
+ originIndex !== undefined &&
103
+ (!validIndex(originIndex) || originIndex === 0)
104
+ )
105
+ return null
75
106
  if (!Array.isArray(events) || events.length === 0) return null
76
107
  const parsedEvents: ScheduledEvent[] = []
77
108
  for (const event of events) {
@@ -102,5 +133,6 @@ export function normalizeSchedulerTask(value: unknown): SchedulerTask | null {
102
133
  ...base,
103
134
  dueAt,
104
135
  events: parsedEvents,
136
+ ...(originIndex === undefined ? {} : { originIndex }),
105
137
  }
106
138
  }
@@ -10,8 +10,8 @@
10
10
  * - append tasks hash their stable task id with the topic for provider
11
11
  * deduplication. Their six-day maximum delay leaves a full day for
12
12
  * delivery and retries inside the provider's seven-day retention.
13
- * They target the publishing deployment unless explicitly configured
14
- * as deploymentless. Drain tasks always remain deployment-scoped.
13
+ * Clear tasks follow the same identity and delay rules. All tasks use
14
+ * the configured deployment scope for sending and receiving.
15
15
  * - `handler(...servers)` is the delivery route: look up the server for
16
16
  * the contract named in the message and drain it. Settled drains ack;
17
17
  * contention arms a later watchdog before acking; handler failure keeps
@@ -38,6 +38,7 @@ import {
38
38
  } from './scheduler-task.ts'
39
39
  import {
40
40
  deliverSchedulerAppend,
41
+ deliverSchedulerClear,
41
42
  type A2Scheduler,
42
43
  type DrainableServer,
43
44
  } from './server.ts'
@@ -58,14 +59,6 @@ export type QueueDeliveryMetadata = {
58
59
  deliveryCount: number
59
60
  }
60
61
 
61
- type VercelQueueMessage = SchedulerTask & {
62
- /** Provider-private state used to address callback lease operations. */
63
- _a2Vercel?: {
64
- version: 1
65
- deployment: 'deployment' | 'deploymentless'
66
- }
67
- }
68
-
69
62
  /** What this module needs from a Vercel Queues-compatible transport. */
70
63
  export type QueueTransport = {
71
64
  send(
@@ -76,10 +69,6 @@ export type QueueTransport = {
76
69
  idempotencyKey?: string
77
70
  retentionSeconds?: number
78
71
  },
79
- /** Resolved A2 affinity. Custom transports own its provider mapping. */
80
- routing?: {
81
- deployment: 'deployment' | 'deploymentless'
82
- },
83
72
  ): Promise<unknown>
84
73
  /**
85
74
  * Wrap the delivery handler into a route. The handler throws
@@ -101,11 +90,8 @@ export type QueueTransport = {
101
90
  export type VercelQueuesOptions = {
102
91
  /** Queue topic matching `[A-Za-z0-9_-]+`. Default `'a2'`. */
103
92
  topic?: string
104
- /**
105
- * Deployment affinity for scheduled append tasks. Recovery drain tasks
106
- * always target the publishing deployment. Default `'deployment'`.
107
- */
108
- scheduledAppends?: 'deployment' | 'deploymentless'
93
+ /** Queue SDK deployment scope for all tasks. Default: VERCEL_DEPLOYMENT_ID; null opts out. */
94
+ deploymentId?: string | null
109
95
  /** Injectable transport (tests). Default: `@vercel/queue`. */
110
96
  transport?: QueueTransport
111
97
  /** Injectable clock for due-time calculations (tests). */
@@ -200,7 +186,9 @@ const unsettledBackoffSeconds = (deliveryCount: number): number =>
200
186
  60,
201
187
  )
202
188
 
203
- const vercelQueueTransport = async (): Promise<QueueTransport> => {
189
+ const vercelQueueTransport = async (
190
+ clientOptions: Pick<VercelQueuesOptions, 'deploymentId'>,
191
+ ): Promise<QueueTransport> => {
204
192
  const queue = await import('@vercel/queue').catch(() => {
205
193
  throw classifySchedulerSendFailure(
206
194
  new Error(
@@ -209,23 +197,11 @@ const vercelQueueTransport = async (): Promise<QueueTransport> => {
209
197
  'terminal',
210
198
  )
211
199
  })
212
- const deploymentClient = new queue.QueueClient()
213
- const deploymentlessClient = new queue.QueueClient({ deploymentId: null })
200
+ const client = new queue.QueueClient(clientOptions)
214
201
  return {
215
- send: (topic, message, options, routing) => {
216
- const deployment = routing?.deployment ?? 'deployment'
217
- const body: VercelQueueMessage = {
218
- ...message,
219
- _a2Vercel: { version: 1, deployment },
220
- }
221
- return (
222
- deployment === 'deploymentless'
223
- ? deploymentlessClient
224
- : deploymentClient
225
- ).send(topic, body, options)
226
- },
227
- handleCallback: (handler) => {
228
- const callbackOptions = {
202
+ send: (topic, message, options) => client.send(topic, message, options),
203
+ handleCallback: (handler) =>
204
+ client.handleCallback(handler, {
229
205
  // A hard-killed callback normally has a fresh claim watchdog. If
230
206
  // its successor send also failed, make the old unacked delivery's
231
207
  // fallback as prompt as Vercel Queues permits.
@@ -234,36 +210,13 @@ const vercelQueueTransport = async (): Promise<QueueTransport> => {
234
210
  // unacked) delivery via a visibility change — 200, no error
235
211
  // log. Everything else returns undefined → the SDK's default
236
212
  // throw path, which is loud on purpose.
237
- retry: (error: unknown, metadata: QueueDeliveryMetadata) =>
213
+ retry: (error, metadata) =>
238
214
  error instanceof DrainUnsettled
239
215
  ? {
240
216
  afterSeconds: unsettledBackoffSeconds(metadata.deliveryCount),
241
217
  }
242
218
  : undefined,
243
- }
244
- const deploymentHandler = deploymentClient.handleCallback(
245
- handler,
246
- callbackOptions,
247
- ) as (req: Request) => Promise<Response>
248
- const deploymentlessHandler = deploymentlessClient.handleCallback(
249
- handler,
250
- callbackOptions,
251
- ) as (req: Request) => Promise<Response>
252
- return async (req: Request): Promise<Response> => {
253
- let delivered: unknown
254
- try {
255
- delivered = await req.clone().json()
256
- } catch {
257
- return deploymentlessHandler(req)
258
- }
259
- const metadata = recordOf(recordOf(delivered)?.['_a2Vercel'])
260
- const routing =
261
- metadata?.['version'] === 1 ? metadata['deployment'] : undefined
262
- return routing === 'deployment'
263
- ? deploymentHandler(req)
264
- : deploymentlessHandler(req)
265
- }
266
- },
219
+ }) as (req: Request) => Promise<Response>,
267
220
  isDuplicate: (error) => error instanceof queue.DuplicateMessageError,
268
221
  }
269
222
  }
@@ -273,23 +226,18 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
273
226
  if (typeof topic !== 'string' || !TOPIC_PATTERN.test(topic)) {
274
227
  throw new TypeError('vercelQueues() topic must match [A-Za-z0-9_-]+')
275
228
  }
276
- const scheduledAppends = options.scheduledAppends ?? 'deployment'
277
- if (
278
- scheduledAppends !== 'deployment' &&
279
- scheduledAppends !== 'deploymentless'
280
- ) {
281
- throw new TypeError(
282
- "vercelQueues() scheduledAppends must be 'deployment' or 'deploymentless'",
283
- )
284
- }
285
229
  const clock = options.clock ?? SYSTEM_CLOCK
230
+ const clientOptions =
231
+ options.deploymentId === undefined
232
+ ? {}
233
+ : { deploymentId: options.deploymentId }
286
234
 
287
235
  // Lazy: constructing the scheduler value does no I/O and works without
288
236
  // the peer dep until the first task or delivery.
289
237
  const transport = retryableLazy(() =>
290
238
  options.transport
291
239
  ? Promise.resolve(options.transport)
292
- : vercelQueueTransport(),
240
+ : vercelQueueTransport(clientOptions),
293
241
  ).get
294
242
 
295
243
  const sendTask = async (task: SchedulerTask): Promise<void> => {
@@ -305,7 +253,7 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
305
253
  requestedDueAt <= nowMs
306
254
  ? Math.floor(nowMs / 1_000) * 1_000
307
255
  : Math.ceil(requestedDueAt / 1_000) * 1_000
308
- const deliveryTargetAt = task.kind === 'append' ? requestedDueAt : dueAt
256
+ const deliveryTargetAt = task.kind === 'drain' ? dueAt : requestedDueAt
309
257
  const deliveryDelaySeconds = Math.max(
310
258
  0,
311
259
  Math.ceil((deliveryTargetAt - nowMs) / 1_000),
@@ -318,7 +266,6 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
318
266
  'terminal',
319
267
  )
320
268
  }
321
- const deployment = task.kind === 'append' ? scheduledAppends : 'deployment'
322
269
  const message: SchedulerTask = { ...task, dueAt }
323
270
  const idempotencyKey =
324
271
  task.kind === 'drain'
@@ -329,19 +276,16 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
329
276
  task.sessionId,
330
277
  String(dueAt),
331
278
  ])
332
- : digestId(['a2:scheduler-vercel:append:v1', topic, task.id])
279
+ : task.kind === 'append'
280
+ ? digestId(['a2:scheduler-vercel:append:v1', topic, task.id])
281
+ : digestId(['a2:scheduler-vercel:clear:v1', topic, task.id])
333
282
  const t = await transport()
334
283
  try {
335
- await t.send(
336
- topic,
337
- message,
338
- {
339
- delaySeconds: deliveryDelaySeconds,
340
- idempotencyKey,
341
- retentionSeconds: MAX_RETENTION_SECONDS,
342
- },
343
- { deployment },
344
- )
284
+ await t.send(topic, message, {
285
+ delaySeconds: deliveryDelaySeconds,
286
+ idempotencyKey,
287
+ retentionSeconds: MAX_RETENTION_SECONDS,
288
+ })
345
289
  } catch (err) {
346
290
  if (t.isDuplicate(err)) return
347
291
  throw classifyVercelSendFailure(err)
@@ -394,6 +338,10 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
394
338
  await deliverSchedulerAppend(server, task)
395
339
  return
396
340
  }
341
+ if (task.kind === 'clear') {
342
+ await deliverSchedulerClear(server, task)
343
+ return
344
+ }
397
345
 
398
346
  const nextDueAt = successorDueAt(task)
399
347
  const result = internals
@@ -1,3 +1,4 @@
1
+ import { encodeState } from './reducer-runtime.ts'
1
2
  import type {
2
3
  AppendInput,
3
4
  ContractEvent,
@@ -12,7 +13,7 @@ import {
12
13
  type ParsedPushEvent,
13
14
  type ParsedPushPresence,
14
15
  } from './push-envelope.ts'
15
- import type { A2Server } from './server.ts'
16
+ import type { A2Server, Session, SessionAppend } from './server.ts'
16
17
  import {
17
18
  sessionSocket,
18
19
  sessionsSocket,
@@ -20,7 +21,16 @@ import {
20
21
  type SocketSession,
21
22
  } from './session-socket.ts'
22
23
  import { sseResponse } from './sse.ts'
23
- import { asA2Error, errorStatus, errorToWire, eventToWire } from './wire.ts'
24
+ import { bootstrapStream } from './bootstrap-stream.ts'
25
+ import type { Reducer } from './reducer.ts'
26
+ import type { SubscriptionStart, WireVersion } from './wire.ts'
27
+ import {
28
+ asA2Error,
29
+ errorStatus,
30
+ errorToWire,
31
+ eventToWire,
32
+ wireVersion,
33
+ } from './wire.ts'
24
34
 
25
35
  type SuggestedEventType<D extends EventDefs> =
26
36
  (keyof D & string) | (string & Record<never, never>)
@@ -50,6 +60,7 @@ export type A2Operation<
50
60
  readonly type: 'stream'
51
61
  readonly sessionId: string
52
62
  readonly startAfter: number
63
+ readonly bootstrap?: true
53
64
  readonly transport: 'http' | 'websocket'
54
65
  }
55
66
  | {
@@ -100,10 +111,15 @@ export function setServerFetchHooks<
100
111
  serverFetchHooks.set(server.fetch, hooks)
101
112
  }
102
113
 
103
- type FetchServer<D extends EventDefs, P extends PresenceDefs> = Pick<
104
- A2Server<D, P>,
105
- 'contract' | 'session'
106
- >
114
+ type FetchServer<D extends EventDefs, P extends PresenceDefs> = {
115
+ contract: A2Server<D, P>['contract']
116
+ session(id: string): Session<D, SessionAppend<D>, P>
117
+ /** Public history plus the latest clear marker when the range includes it. */
118
+ historyPage(
119
+ sessionId: string,
120
+ bounds: { gte: number; lte: number },
121
+ ): Promise<{ events: ContractEvent<D>[]; clearIndex?: number }>
122
+ }
107
123
 
108
124
  type ServerFetch<D extends EventDefs, P extends PresenceDefs> = {
109
125
  (request: Request): Promise<Response>
@@ -150,6 +166,25 @@ const parseResumeIndex = (raw: string | null): number => {
150
166
  return index
151
167
  }
152
168
 
169
+ const parseSubscription = (params: URLSearchParams): SubscriptionStart => {
170
+ const reducer = params.get('reducer')
171
+ const bootstrap = params.get('bootstrap')
172
+ const index = params.get('index')
173
+ if (reducer === '') throw new A2Error('INVALID_PAYLOAD', 'empty reducer name')
174
+ if (bootstrap !== null) {
175
+ if (bootstrap !== '1' || index !== null || reducer === null)
176
+ throw new A2Error(
177
+ 'INVALID_PAYLOAD',
178
+ 'bootstrap requires a reducer and no index',
179
+ )
180
+ return { bootstrap: true, reducer }
181
+ }
182
+ return {
183
+ index: parseResumeIndex(index),
184
+ ...(reducer === null ? {} : { reducer }),
185
+ }
186
+ }
187
+
153
188
  const errorResponse = (error: unknown): Response => {
154
189
  const a2error = asA2Error(error)
155
190
  return Response.json(errorToWire(a2error), {
@@ -235,29 +270,82 @@ export function createServerFetch<
235
270
  }
236
271
  }
237
272
 
238
- const attachMultiplexed = (socket: A2Socket): void => {
239
- const deadline = invocationDeadlineMs()
240
- sessionsSocket<D>(
241
- async (sessionId, startAfter) => {
242
- const operation: A2Operation<D, P> = {
243
- type: 'stream',
244
- sessionId,
245
- startAfter,
246
- transport: 'websocket',
247
- }
248
- return (await authorized(operation))
249
- ? server.session(sessionId)
250
- : null
251
- },
252
- socket,
253
- {
254
- presence,
255
- ...(deadline === null ? {} : { deadline }),
256
- gatePush: gateSocketPush,
257
- },
273
+ const selectedSession = (
274
+ sessionId: string,
275
+ selection: SubscriptionStart,
276
+ ): SocketSession => {
277
+ const reducer = server.contract.reducer
278
+ if (
279
+ selection.reducer !== undefined &&
280
+ reducer !== undefined &&
281
+ reducer.name !== selection.reducer
258
282
  )
283
+ throw new A2Error(
284
+ 'INVALID_PAYLOAD',
285
+ 'client reducer does not match the public contract reducer',
286
+ )
287
+ const target = server.session(sessionId)
288
+ const streamTarget = target as unknown as SocketSession
289
+ if (!selection.bootstrap) return streamTarget
290
+ if (!reducer)
291
+ throw new A2Error('INVALID_PAYLOAD', 'contract has no public reducer')
292
+ return {
293
+ ...streamTarget,
294
+ stream: (streamOptions) =>
295
+ bootstrapStream({
296
+ snapshot: async () => {
297
+ const initial = await target.state(
298
+ reducer as unknown as Reducer<D, unknown>,
299
+ )
300
+ return {
301
+ kind: 'snapshot',
302
+ sessionId,
303
+ name: reducer.name,
304
+ index: initial.index,
305
+ state: encodeState({ reducer, state: initial.state }),
306
+ ...(initial.clearIndex === undefined
307
+ ? {}
308
+ : { clearIndex: initial.clearIndex }),
309
+ }
310
+ },
311
+ stream: (index) => {
312
+ const bounds = {
313
+ startAfter: index,
314
+ presence: streamOptions.presence === true,
315
+ }
316
+ return target.stream(bounds)
317
+ },
318
+ }),
319
+ }
259
320
  }
260
321
 
322
+ const attachMultiplexed =
323
+ (wire: WireVersion) =>
324
+ (socket: A2Socket): void => {
325
+ const deadline = invocationDeadlineMs()
326
+ sessionsSocket<D>(
327
+ async (sessionId, startAfter, selection = { index: startAfter }) => {
328
+ const operation: A2Operation<D, P> = {
329
+ type: 'stream',
330
+ sessionId,
331
+ startAfter,
332
+ ...(selection.bootstrap ? { bootstrap: true as const } : {}),
333
+ transport: 'websocket',
334
+ }
335
+ return (await authorized(operation))
336
+ ? selectedSession(sessionId, selection)
337
+ : null
338
+ },
339
+ socket,
340
+ {
341
+ presence,
342
+ wire,
343
+ ...(deadline === null ? {} : { deadline }),
344
+ gatePush: gateSocketPush,
345
+ },
346
+ )
347
+ }
348
+
261
349
  try {
262
350
  if (request.method === 'GET') {
263
351
  if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') {
@@ -267,28 +355,32 @@ export function createServerFetch<
267
355
  { status: 426 },
268
356
  )
269
357
  }
358
+ const { searchParams } = new URL(request.url)
359
+ const wire = wireVersion(searchParams.get('wire'))
270
360
  if (options.multiplexWebSocket === true) {
271
- return await options.upgradeWebSocket(attachMultiplexed)
361
+ return await options.upgradeWebSocket(attachMultiplexed(wire))
272
362
  }
273
363
 
274
- const { searchParams } = new URL(request.url)
275
364
  const sessionId = searchParams.get('sessionId')
276
365
  if (sessionId === null || sessionId.length === 0) {
277
366
  throw new A2Error('INVALID_PAYLOAD', 'missing sessionId')
278
367
  }
279
- const startAfter = parseResumeIndex(searchParams.get('index'))
368
+ const selection = parseSubscription(searchParams)
369
+ const startAfter = selection.index ?? 0
280
370
  await authorize({
281
371
  type: 'stream',
282
372
  sessionId,
283
373
  startAfter,
374
+ ...(selection.bootstrap ? { bootstrap: true as const } : {}),
284
375
  transport: 'websocket',
285
376
  })
286
- const target = server.session(sessionId)
377
+ const target = selectedSession(sessionId, selection)
287
378
  const attach = (socket: A2Socket): void => {
288
379
  const deadline = invocationDeadlineMs()
289
380
  sessionSocket(sessionId, target, socket, {
290
381
  startAfter,
291
382
  presence,
383
+ wire,
292
384
  ...(deadline === null ? {} : { deadline }),
293
385
  gatePush: gateSocketPush,
294
386
  })
@@ -311,25 +403,37 @@ export function createServerFetch<
311
403
  ...bounds,
312
404
  transport: 'http',
313
405
  })
314
- const events = await server.session(sessionId).history(bounds)
315
- const covered = events.length === bounds.lte - bounds.gte + 1
406
+ const { events, clearIndex } = await server.historyPage(
407
+ sessionId,
408
+ bounds,
409
+ )
410
+ const first = Math.max(bounds.gte, (clearIndex ?? 0) + 1)
411
+ const covered = events.length === Math.max(0, bounds.lte - first + 1)
316
412
  return Response.json(events.map(eventToWire), {
317
- headers: { 'a2-history-covered': String(covered) },
413
+ headers: {
414
+ 'a2-history-covered': String(covered),
415
+ ...(clearIndex === undefined
416
+ ? {}
417
+ : { 'a2-clear-index': String(clearIndex) }),
418
+ },
318
419
  })
319
420
  }
320
421
 
321
- const startAfter = parseResumeIndex(searchParams.get('index'))
422
+ const selection = parseSubscription(searchParams)
423
+ const startAfter = selection.index ?? 0
322
424
  await authorize({
323
425
  type: 'stream',
324
426
  sessionId,
325
427
  startAfter,
428
+ ...(selection.bootstrap ? { bootstrap: true as const } : {}),
326
429
  transport: 'http',
327
430
  })
328
- const target = server.session(sessionId) as unknown as SocketSession
431
+ const target = selectedSession(sessionId, selection)
329
432
  return sseResponse(
330
433
  presence
331
434
  ? target.stream({ startAfter, presence: true })
332
435
  : target.stream({ startAfter }),
436
+ { wire: wireVersion(searchParams.get('wire')) },
333
437
  )
334
438
  }
335
439