experimental-a2 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
  4. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/actor-shared-DFET5q16.js.map +1 -0
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
  17. package/dist/ai-EEfKyc-z.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +4 -2
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +214 -67
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  25. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  26. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +3 -3
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +86 -73
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
  40. package/dist/client-BQS4i_V9.js.map +1 -0
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
  52. package/dist/internal-CvsrprnU.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-CIeL33wj.js +12 -0
  65. package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.js +3 -3
  68. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  69. package/dist/scheduler-vercel.d.ts +2 -2
  70. package/dist/scheduler-vercel.js +3 -3
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
  73. package/dist/server-DtFrWxi3.js.map +1 -0
  74. package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
  75. package/dist/server-qgez0Xny.d.ts.map +1 -0
  76. package/dist/server.browser.js +1 -1
  77. package/dist/server.browser.js.map +1 -1
  78. package/dist/server.d.ts +4 -4
  79. package/dist/server.js +2 -2
  80. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  81. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  82. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  83. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  84. package/dist/store-D0MhBvg6.d.ts +532 -0
  85. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js.map +1 -1
  87. package/dist/store-codec-DwrWlgQx.js +65 -0
  88. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +406 -225
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
  94. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +1598 -587
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  100. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.js +4 -4
  103. package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
  104. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  105. package/dist/store-redis.d.ts +1 -1
  106. package/dist/store-redis.d.ts.map +1 -1
  107. package/dist/store-redis.js +4 -5
  108. package/dist/store-redis.js.map +1 -1
  109. package/dist/store-sqlite.d.ts +1 -1
  110. package/dist/store-sqlite.d.ts.map +1 -1
  111. package/dist/store-sqlite.js +527 -299
  112. package/dist/store-sqlite.js.map +1 -1
  113. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  114. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  115. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  116. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  117. package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
  118. package/dist/wire-CV-Z7wz-.js.map +1 -0
  119. package/docs/01-quickstart.mdx +21 -15
  120. package/docs/02-examples.mdx +1 -1
  121. package/docs/actors/01-introduction.mdx +13 -2
  122. package/docs/actors/02-concurrency.mdx +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +12 -11
  125. package/docs/agents/03-conversations.mdx +36 -25
  126. package/docs/agents/04-harnesses.mdx +14 -7
  127. package/docs/client/03-presence.mdx +18 -14
  128. package/docs/contract/01-events.mdx +2 -2
  129. package/docs/contract/02-state.mdx +18 -14
  130. package/docs/contract/03-sessions.mdx +32 -25
  131. package/docs/index.mdx +29 -24
  132. package/docs/reference/01-core.mdx +7 -6
  133. package/docs/reference/02-server.mdx +250 -191
  134. package/docs/reference/04-ai.mdx +33 -23
  135. package/docs/reference/05-telemetry.mdx +27 -13
  136. package/docs/reference/06-errors.mdx +25 -19
  137. package/docs/reference/meta.ts +1 -1
  138. package/docs/schedulers/01-overview.mdx +8 -8
  139. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  140. package/docs/schedulers/03-qstash.mdx +4 -4
  141. package/docs/server/01-consumers.mdx +418 -0
  142. package/docs/server/02-timers.mdx +31 -21
  143. package/docs/server/03-cancellation.mdx +52 -44
  144. package/docs/server/04-routes.mdx +4 -1
  145. package/docs/server/05-sql-projections.mdx +391 -0
  146. package/docs/server/06-guarantees.mdx +61 -51
  147. package/docs/stores/01-overview.mdx +3 -3
  148. package/docs/stores/02-redis.mdx +24 -10
  149. package/docs/stores/03-redis-http.mdx +8 -6
  150. package/docs/stores/04-postgres.mdx +65 -14
  151. package/docs/stores/05-sqlite.mdx +5 -3
  152. package/docs/upgrades/01-overview.mdx +31 -0
  153. package/docs/upgrades/78-0-22.mdx +115 -0
  154. package/docs/upgrades/79-0-21.mdx +165 -0
  155. package/docs/upgrades/80-0-20.mdx +43 -0
  156. package/docs/upgrades/81-0-19.mdx +33 -0
  157. package/docs/upgrades/82-0-18.mdx +33 -0
  158. package/docs/upgrades/83-0-17.mdx +69 -0
  159. package/docs/upgrades/meta.ts +3 -0
  160. package/examples/playground/DEPLOY.md +39 -7
  161. package/examples/playground/app/a0/chat.test.tsx +2 -2
  162. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  163. package/examples/playground/app/a0/sandbox.ts +58 -13
  164. package/examples/playground/app/a0/server.ts +4 -4
  165. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  166. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  167. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  168. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  169. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  170. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  171. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  172. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  173. package/examples/playground/app/a2slack/db/a2.ts +209 -0
  174. package/examples/playground/app/a2slack/db/index.ts +73 -0
  175. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  176. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  177. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  178. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  179. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  180. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  181. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  182. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  183. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  184. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
  186. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  187. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  188. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  189. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  190. package/examples/playground/app/a2slack/events/route.ts +36 -0
  191. package/examples/playground/app/a2slack/identity.ts +44 -0
  192. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  193. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  194. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  195. package/examples/playground/app/a2slack/lag.ts +73 -0
  196. package/examples/playground/app/a2slack/layout.tsx +43 -0
  197. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  198. package/examples/playground/app/a2slack/model.test.ts +208 -0
  199. package/examples/playground/app/a2slack/model.ts +369 -0
  200. package/examples/playground/app/a2slack/models.test.ts +72 -0
  201. package/examples/playground/app/a2slack/page.tsx +10 -0
  202. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  203. package/examples/playground/app/a2slack/search/route.ts +40 -0
  204. package/examples/playground/app/a2slack/search-index.ts +108 -0
  205. package/examples/playground/app/a2slack/server.test.ts +315 -0
  206. package/examples/playground/app/a2slack/server.ts +55 -0
  207. package/examples/playground/app/a2slack/sessions.ts +50 -0
  208. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  209. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  210. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  211. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  212. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  213. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  214. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  215. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  216. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  217. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  218. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  219. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  220. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  221. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  222. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  223. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  224. package/examples/playground/app/agent/server.ts +3 -3
  225. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  226. package/examples/playground/app/chat/server.ts +45 -35
  227. package/examples/playground/app/components/demo-icons.tsx +7 -0
  228. package/examples/playground/app/components/demos.tsx +7 -0
  229. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  230. package/examples/playground/app/orders/page.tsx +1 -1
  231. package/examples/playground/app/orders/server.ts +49 -30
  232. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  233. package/examples/playground/app/recovery/page.tsx +4 -4
  234. package/examples/playground/app/recovery/server.ts +23 -32
  235. package/examples/playground/drizzle.config.ts +14 -0
  236. package/examples/playground/lib/store.ts +1 -1
  237. package/examples/playground/next.config.ts +6 -1
  238. package/examples/playground/package.json +12 -1
  239. package/examples/playground/scripts/migrate.mjs +31 -0
  240. package/examples/playground/vercel.json +9 -0
  241. package/examples/playground/vitest.drills.config.ts +32 -0
  242. package/package.json +5 -2
  243. package/sql/postgres/drizzle-schema.ts +209 -0
  244. package/sql/postgres/schema.sql +227 -67
  245. package/src/actor-shared.ts +10 -2
  246. package/src/actor.ts +101 -71
  247. package/src/ai-agent.ts +162 -111
  248. package/src/ai-control-server.ts +8 -8
  249. package/src/ai-fx.ts +324 -82
  250. package/src/ai-harness.ts +17 -2
  251. package/src/ai-pi.ts +2 -2
  252. package/src/ai-server.ts +2 -2
  253. package/src/ai.ts +7 -7
  254. package/src/client.ts +19 -2
  255. package/src/contract.ts +1 -1
  256. package/src/deterministic-id.ts +27 -4
  257. package/src/errors.ts +8 -5
  258. package/src/idempotent-replay.ts +0 -1
  259. package/src/index.ts +1 -1
  260. package/src/internal.ts +57 -16
  261. package/src/otel.ts +1 -1
  262. package/src/postgres-schema.ts +98 -57
  263. package/src/presence-recovery.ts +18 -11
  264. package/src/presence-tombstones.ts +52 -0
  265. package/src/push-envelope.ts +20 -0
  266. package/src/runtime-consumer.ts +10 -0
  267. package/src/scheduler-task.ts +2 -2
  268. package/src/scheduler-vercel.ts +1 -1
  269. package/src/server.browser.ts +1 -1
  270. package/src/server.ts +819 -218
  271. package/src/session-socket.ts +1 -1
  272. package/src/store-codec.ts +66 -0
  273. package/src/store-memory.ts +604 -286
  274. package/src/store-postgres.ts +2006 -857
  275. package/src/store-presence-polling.ts +59 -34
  276. package/src/store-redis-core.ts +1961 -1333
  277. package/src/store-redis-notify.ts +76 -26
  278. package/src/store-redis.ts +2 -3
  279. package/src/store-sqlite.ts +915 -516
  280. package/src/store.ts +241 -102
  281. package/src/telemetry.ts +2 -1
  282. package/src/wire.ts +1 -0
  283. package/dist/actor-C7b-1arn.d.ts.map +0 -1
  284. package/dist/actor-shared-t7w8xIKP.js.map +0 -1
  285. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  286. package/dist/ai-mMiWul1T.js.map +0 -1
  287. package/dist/client-BhHLoNKF.js.map +0 -1
  288. package/dist/contract-BYKASlCV.js.map +0 -1
  289. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  290. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  291. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  292. package/dist/internal-CscSY7K_.js.map +0 -1
  293. package/dist/server-DYAATFuT.d.ts.map +0 -1
  294. package/dist/server-qyb5aJJG.js.map +0 -1
  295. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  296. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  297. package/dist/store-CyLUy86v.d.ts +0 -404
  298. package/dist/store-CyLUy86v.d.ts.map +0 -1
  299. package/dist/store-codec-DTG0Ftek.js +0 -10
  300. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  301. package/dist/store-presence-polling-B1ixeqIS.js +0 -94
  302. package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
  303. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  304. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  305. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  306. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
  307. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  308. package/dist/wire-BKxH1vbP.js.map +0 -1
  309. package/docs/server/01-handlers.mdx +0 -227
@@ -24,13 +24,31 @@ Redis, poll cadence elsewhere. Upstash is durable by default; use its TCP
24
24
  `rediss://` URL, not the REST one. Each feature creates its own server with
25
25
  the shared store; storage remains namespaced per contract. Without either variable, a
26
26
  production boot fails fast with `STORE_NOT_CONFIGURED` (by design).
27
- Redis needs no setup. With Postgres, create A2's tables once before the
28
- first deploy (A2 never creates them at runtime):
27
+ Redis needs no setup. Postgres is migrated by every Vercel build: the
28
+ `vercel-build` script runs `scripts/migrate.mjs` (Drizzle's migrator, which,
29
+ unlike `drizzle-kit migrate`, prints the error when one fails) before the
30
+ usual `turbo run build`, through `DATABASE_URL_UNPOOLED` when it's set (the Neon
31
+ integration's direct connection). Previews and production share one database here, so a branch's
32
+ migration reaches it when its preview builds: keep migrations additive.
33
+ Elsewhere, migrate by hand (A2 never creates its tables at runtime):
29
34
 
30
35
  ```sh
31
- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f node_modules/experimental-a2/sql/postgres/schema.sql
36
+ DATABASE_URL=postgres://… pnpm --filter playground db:migrate
32
37
  ```
33
38
 
39
+ It applies every migration in `app/a2slack/db/migrations`. A2's tables are in
40
+ the Drizzle schema with a2slack's: `app/a2slack/db/a2.ts` is a copy of
41
+ `experimental-a2`'s `sql/postgres/drizzle-schema.ts`. drizzle-kit generated
42
+ their migration (`0002_a2.sql`) from it, and its SQL is `schema.sql`'s
43
+ instead of the generated `CREATE TABLE`s, because the shared database already
44
+ had A2's tables: it creates what's missing and upgrades the rest. Drizzle
45
+ can't declare A2's `a2_batch` function, so `0003_a2_batch.sql` is a custom
46
+ migration (`drizzle-kit generate --custom`) holding `schema.sql`'s block for
47
+ it. When a
48
+ release of `experimental-a2` changes its tables (its changelog says so),
49
+ update `a2.ts` from the new file, run `pnpm db:generate`, and read the SQL it
50
+ writes.
51
+
34
52
  To verify a provider before leaning on it, run the full conformance
35
53
  kit against it (throwaway database; the suite isolates by key prefix
36
54
  and sweeps after itself):
@@ -42,6 +60,19 @@ TEST_REDIS_URL=rediss://… pnpm --filter a2 exec vitest run test/store-redis.co
42
60
  `pg` and `ioredis` are optional peer dependencies of `experimental-a2` and direct
43
61
  dependencies of this app.
44
62
 
63
+ a2slack keeps its tables in the Postgres at `DATABASE_URL`, defined with
64
+ Drizzle in `app/a2slack/db/schema.ts`. With `REDIS_URL` set too, A2 runs on
65
+ Redis and only a2slack's tables are used in Postgres (A2's are created there
66
+ too, and stay empty). Generate a migration with `pnpm db:generate` after
67
+ changing the schema; SQL drizzle-kit can't express, like an extension, goes
68
+ in a custom one (`pnpm db:generate --custom --name …`).
69
+
70
+ Its search index lives in Upstash Redis Search, through the Redis's REST
71
+ credentials (`KV_REST_API_URL` and `KV_REST_API_TOKEN`, or this project's
72
+ `REDIS_KV_REST_API_*`). The index is created the first time it's written.
73
+ Without them, search reads the Postgres table instead. In development the
74
+ tables live in PGlite at `.a2/a2slack`, migrated when it opens.
75
+
45
76
  ## 2. Vercel Queues scheduler
46
77
 
47
78
  Queues need no configuration beyond the triggers in `vercel.json`. A0, Orders,
@@ -153,10 +184,11 @@ vercel deploy
153
184
  ```
154
185
 
155
186
  The Recovery lab tests a real function timeout. Its dedicated trigger and
156
- queue consumer both export `maxDuration = 20`. The pipeline has four 8-second
157
- handlers, so every handler fits in a fresh invocation but the 32-second chain
158
- does not. A2 keeps dispatching events. Vercel stops the first invocation while
159
- the third handler is running, leaving that event without a processed marker.
187
+ queue consumer both export `maxDuration = 20`. The pipeline is four 8-second
188
+ steps, run in order by one A2 consumer, so every step fits in a fresh
189
+ invocation but the 32-second chain does not. A2 keeps dispatching events.
190
+ Vercel stops the first invocation while the third step is running, leaving
191
+ the consumer's cursor on that event.
160
192
  The deadline-capped claim expires, its watchdog retries the same event in a
161
193
  fresh function, and already completed events do not run again. The dedicated
162
194
  `a2-recovery` topic keeps this artificial budget away from the
@@ -10,7 +10,7 @@ import {
10
10
  within,
11
11
  } from '@testing-library/react'
12
12
  import { tool } from 'ai'
13
- import { handlerContext, inputs } from 'experimental-a2/ai'
13
+ import { consumerContext, inputs } from 'experimental-a2/ai'
14
14
  import { agent } from 'experimental-a2/ai/server'
15
15
  import type { A2Operation } from 'experimental-a2/server'
16
16
  import { memory } from 'experimental-a2/store-memory'
@@ -57,7 +57,7 @@ const tools = {
57
57
  bash: tool({
58
58
  inputSchema: z.object({ command: z.string() }),
59
59
  execute: async () => {
60
- const { signal } = handlerContext()
60
+ const { signal } = consumerContext()
61
61
  await new Promise<void>((resolve, reject) => {
62
62
  void bashDone.promise.then(resolve)
63
63
  signal.addEventListener('abort', () => reject(signal.reason), {
@@ -1,7 +1,7 @@
1
1
  import 'server-only'
2
2
  import { contract } from 'experimental-a2'
3
3
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
4
- import { createServer } from 'experimental-a2/server'
4
+ import { consumer, createServer } from 'experimental-a2/server'
5
5
  import { z } from 'zod'
6
6
  import { store } from '@/lib/store'
7
7
  import { isAppId } from './apps'
@@ -31,24 +31,26 @@ export const sandboxScheduler =
31
31
  ? vercelQueues({ topic: 'a2-a0-sandboxes' })
32
32
  : undefined
33
33
 
34
+ const deleteIdleSandbox = consumer({
35
+ contract: sandboxLog,
36
+ name: 'delete-idle-sandbox',
37
+ types: ['sandbox.idle.checked'],
38
+ }).run(async ({ event }) => {
39
+ const { appId } = event.payload
40
+ // A prompt since the sandbox opened makes it a real app's, whichever
41
+ // harness's agent got it.
42
+ const reads = await Promise.all(
43
+ Object.values(agentServers).map((server) => server.session(appId).state()),
44
+ )
45
+ if (reads.every(({ state }) => state.messages.length === 0))
46
+ await deleteAppSandbox({ appId })
47
+ })
48
+
34
49
  export const sandboxServer = createServer({
35
50
  ...(store ? { store } : {}),
36
51
  ...(sandboxScheduler ? { scheduler: sandboxScheduler } : {}),
37
52
  contract: sandboxLog,
38
- handlers: {
39
- 'sandbox.idle.checked': async ({ event }) => {
40
- const { appId } = event.payload
41
- // A prompt since the sandbox opened makes it a real app's, whichever
42
- // harness's agent got it.
43
- const reads = await Promise.all(
44
- Object.values(agentServers).map((server) =>
45
- server.session(appId).state(),
46
- ),
47
- )
48
- if (reads.every(({ state }) => state.messages.length === 0))
49
- await deleteAppSandbox({ appId })
50
- },
51
- },
53
+ consumers: [deleteIdleSandbox],
52
54
  })
53
55
 
54
56
  /**
@@ -68,6 +68,7 @@ async function openApp(appId: string): Promise<Sandbox> {
68
68
  persistent: true,
69
69
  snapshotExpiration: SNAPSHOT_EXPIRATION_MS,
70
70
  keepLastSnapshots: { count: 1 },
71
+ onResume: startDev,
71
72
  })
72
73
  } catch (error) {
73
74
  // Another instance created it first. The API holds a racing create until
@@ -81,8 +82,54 @@ async function openApp(appId: string): Promise<Sandbox> {
81
82
  return sandbox
82
83
  }
83
84
 
84
- // Parallel tool calls on a new app share one create within this instance.
85
- const opening = new Map<string, Promise<Sandbox>>()
85
+ // Each instance keeps one handle per app, so parallel tool calls share an
86
+ // open and later ones skip the lookup. A handle resumes a stopped session on
87
+ // its next call; a deleted sandbox fails with a 404 until it's forgotten.
88
+ // An empty app's idle check deletes its sandbox from whichever instance runs
89
+ // it, 10 minutes after the sandbox opened. By then the session's 15 minutes
90
+ // leave less than KEEP_ALIVE_MS, so every call extends it, and the extension's
91
+ // 404 is how a kept handle finds out.
92
+ const handles = new Map<string, Promise<Sandbox>>()
93
+ const MAX_HANDLES = 100
94
+
95
+ const forget = (appId: string): void => {
96
+ handles.delete(appId)
97
+ }
98
+
99
+ const open = (appId: string): Promise<Sandbox> => {
100
+ const opened = openApp(appId)
101
+ opened.catch(() => {
102
+ if (handles.get(appId) === opened) forget(appId)
103
+ })
104
+ return opened
105
+ }
106
+
107
+ const kept = (appId: string): Promise<Sandbox> => {
108
+ const pending = handles.get(appId) ?? open(appId)
109
+ // Most recently used last, so the cap drops the stalest.
110
+ handles.delete(appId)
111
+ handles.set(appId, pending)
112
+ const stalest = handles.keys().next().value
113
+ if (handles.size > MAX_HANDLES && stalest !== undefined) forget(stalest)
114
+ return pending
115
+ }
116
+
117
+ /**
118
+ * Whether the sandbox is still there. Extends it only by the shortfall, so
119
+ * repeated calls never stack up time.
120
+ */
121
+ const keepAlive = async (sandbox: Sandbox): Promise<boolean> => {
122
+ const shortfall =
123
+ Date.now() + KEEP_ALIVE_MS - (sandbox.expiresAt?.getTime() ?? 0)
124
+ if (shortfall <= 0) return true
125
+ try {
126
+ await sandbox.extendTimeout(shortfall)
127
+ } catch (error) {
128
+ // Throws past the plan's session limit; the sandbox still works until then.
129
+ return !notFound(error)
130
+ }
131
+ return true
132
+ }
86
133
 
87
134
  /** The app's sandbox, created or resumed, and kept up while it's used. */
88
135
  export async function appSandbox({
@@ -90,16 +137,13 @@ export async function appSandbox({
90
137
  }: {
91
138
  appId: string
92
139
  }): Promise<Sandbox> {
93
- const pending =
94
- opening.get(appId) ?? openApp(appId).finally(() => opening.delete(appId))
95
- opening.set(appId, pending)
140
+ const pending = kept(appId)
96
141
  const sandbox = await pending
97
- // Extends only by the shortfall, so repeated calls never stack up time.
98
- const shortfall =
99
- Date.now() + KEEP_ALIVE_MS - (sandbox.expiresAt?.getTime() ?? 0)
100
- // Throws past the plan's session limit; the sandbox still works until then.
101
- if (shortfall > 0) await sandbox.extendTimeout(shortfall).catch(() => {})
102
- return sandbox
142
+ if (await keepAlive(sandbox)) return sandbox
143
+ if (handles.get(appId) === pending) forget(appId)
144
+ const reopened = await kept(appId)
145
+ await keepAlive(reopened)
146
+ return reopened
103
147
  }
104
148
 
105
149
  // A server that's booting refuses connections for a moment, and a page
@@ -118,12 +162,12 @@ export async function ensureApp({
118
162
  appId: string
119
163
  }): Promise<{ up: boolean; createdAt: Date }> {
120
164
  const sandbox = await appSandbox({ appId })
121
- const probe = await sandbox.runCommand({
165
+ const answer = await sandbox.runCommand({
122
166
  cmd: 'bash',
123
167
  args: ['-c', `${START_DEV}; ${PROBE}`],
124
168
  })
125
169
  return {
126
- up: Number(await probe.stdout()) > 0,
170
+ up: Number(await answer.stdout()) > 0,
127
171
  createdAt: sandbox.createdAt,
128
172
  }
129
173
  }
@@ -145,6 +189,7 @@ export async function deleteAppSandbox({
145
189
  }: {
146
190
  appId: string
147
191
  }): Promise<void> {
192
+ forget(appId)
148
193
  try {
149
194
  const sandbox = await Sandbox.get({
150
195
  name: sandboxName(appId),
@@ -1,7 +1,7 @@
1
1
  import 'server-only'
2
2
  import { waitUntil } from '@vercel/functions'
3
3
  import { tool } from 'ai'
4
- import { handlerContext } from 'experimental-a2/ai'
4
+ import { consumerContext } from 'experimental-a2/ai'
5
5
  import { fx } from 'experimental-a2/ai/fx'
6
6
  import { pi } from 'experimental-a2/ai/pi'
7
7
  import { agent } from 'experimental-a2/ai/server'
@@ -55,7 +55,7 @@ const tools = {
55
55
  execute: ({ path }) =>
56
56
  timed(async () =>
57
57
  previewUrl({
58
- sandbox: await appSandbox({ appId: handlerContext().session.id }),
58
+ sandbox: await appSandbox({ appId: consumerContext().session.id }),
59
59
  path,
60
60
  }),
61
61
  ),
@@ -68,7 +68,7 @@ const tools = {
68
68
  timeoutSeconds: z.number().int().min(1).max(240).default(120),
69
69
  }),
70
70
  execute: ({ command, timeoutSeconds }) => {
71
- const context = handlerContext()
71
+ const context = consumerContext()
72
72
  return timed(async () =>
73
73
  bash({
74
74
  sandbox: await appSandbox({ appId: context.session.id }),
@@ -190,7 +190,7 @@ export const listServer = createServer({
190
190
  contract: appList,
191
191
  })
192
192
 
193
- /** The dev tools' crash logs, one per app. They have no handlers. */
193
+ /** The dev tools' crash logs, one per app. They have no consumers. */
194
194
  export const crashServer = createServer({
195
195
  ...(store ? { store } : {}),
196
196
  contract: crashLog,