experimental-a2 0.18.1 → 0.20.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 (143) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/actor.js +1 -1
  3. package/dist/{ai-BMLlI2Ff.d.ts → ai-Cz3hqVhk.d.ts} +33 -16
  4. package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
  5. package/dist/ai-client.d.ts +1 -1
  6. package/dist/ai-client.js +1 -1
  7. package/dist/ai-control-CvLbW10B.js +46 -0
  8. package/dist/ai-control-CvLbW10B.js.map +1 -0
  9. package/dist/ai-fx.d.ts +1 -1
  10. package/dist/ai-fx.d.ts.map +1 -1
  11. package/dist/ai-fx.js +63 -13
  12. package/dist/ai-fx.js.map +1 -1
  13. package/dist/{ai-harness-B2suP2Qh.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
  14. package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
  15. package/dist/{ai-harness-BuLh_EYi.js → ai-harness-Clbg5ei7.js} +88 -15
  16. package/dist/ai-harness-Clbg5ei7.js.map +1 -0
  17. package/dist/{ai-D6mGlO8j.js → ai-mMiWul1T.js} +116 -77
  18. package/dist/ai-mMiWul1T.js.map +1 -0
  19. package/dist/ai-pi.d.ts +1 -1
  20. package/dist/ai-pi.d.ts.map +1 -1
  21. package/dist/ai-pi.js +56 -11
  22. package/dist/ai-pi.js.map +1 -1
  23. package/dist/ai-react.d.ts +1 -1
  24. package/dist/ai-react.js +1 -1
  25. package/dist/ai-server.d.ts +2 -2
  26. package/dist/ai-server.d.ts.map +1 -1
  27. package/dist/ai-server.js +123 -53
  28. package/dist/ai-server.js.map +1 -1
  29. package/dist/ai.d.ts +2 -2
  30. package/dist/ai.js +1 -1
  31. package/dist/internal-CscSY7K_.js.map +1 -1
  32. package/dist/scheduler-qstash.d.ts.map +1 -1
  33. package/dist/scheduler-qstash.js +5 -5
  34. package/dist/scheduler-qstash.js.map +1 -1
  35. package/dist/scheduler-vercel.d.ts.map +1 -1
  36. package/dist/scheduler-vercel.js +5 -7
  37. package/dist/scheduler-vercel.js.map +1 -1
  38. package/dist/server-DYAATFuT.d.ts.map +1 -1
  39. package/dist/{server-D-Y4pqwu.js → server-qyb5aJJG.js} +39 -25
  40. package/dist/server-qyb5aJJG.js.map +1 -0
  41. package/dist/server.js +1 -1
  42. package/dist/session-clear-DGI4jTQ5.js.map +1 -1
  43. package/dist/store-postgres.d.ts.map +1 -1
  44. package/dist/store-postgres.js +38 -136
  45. package/dist/store-postgres.js.map +1 -1
  46. package/docs/01-quickstart.mdx +7 -4
  47. package/docs/actors/01-introduction.mdx +1 -1
  48. package/docs/actors/meta.ts +1 -1
  49. package/docs/agents/02-tools.mdx +1 -1
  50. package/docs/agents/03-conversations.mdx +26 -14
  51. package/docs/agents/04-harnesses.mdx +51 -5
  52. package/docs/agents/meta.ts +1 -1
  53. package/docs/client/meta.ts +1 -1
  54. package/docs/contract/03-sessions.mdx +2 -2
  55. package/docs/index.mdx +2 -2
  56. package/docs/reference/01-core.mdx +8 -8
  57. package/docs/reference/02-server.mdx +3 -3
  58. package/docs/reference/04-ai.mdx +36 -19
  59. package/docs/reference/05-telemetry.mdx +132 -0
  60. package/docs/reference/06-errors.mdx +1 -1
  61. package/docs/reference/meta.ts +1 -1
  62. package/docs/schedulers/01-overview.mdx +112 -0
  63. package/docs/schedulers/02-vercel-queues.mdx +87 -0
  64. package/docs/schedulers/03-qstash.mdx +123 -0
  65. package/docs/schedulers/meta.ts +1 -0
  66. package/docs/server/01-handlers.mdx +29 -2
  67. package/docs/server/02-timers.mdx +3 -3
  68. package/docs/server/06-guarantees.mdx +6 -3
  69. package/docs/stores/01-overview.mdx +37 -0
  70. package/docs/stores/02-redis.mdx +49 -0
  71. package/docs/stores/03-redis-http.mdx +56 -0
  72. package/docs/stores/04-postgres.mdx +121 -0
  73. package/docs/stores/05-sqlite.mdx +34 -0
  74. package/docs/stores/06-memory.mdx +22 -0
  75. package/docs/stores/meta.ts +1 -0
  76. package/examples/playground/DEPLOY.md +19 -5
  77. package/examples/playground/app/a0/a0.css +244 -5
  78. package/examples/playground/app/a0/app-view.tsx +2 -1
  79. package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
  80. package/examples/playground/app/a0/attachments.ts +133 -0
  81. package/examples/playground/app/a0/chat.test.tsx +595 -0
  82. package/examples/playground/app/a0/chat.tsx +623 -63
  83. package/examples/playground/app/a0/context-menu.tsx +38 -11
  84. package/examples/playground/app/a0/model.test.ts +28 -0
  85. package/examples/playground/app/a0/model.ts +30 -0
  86. package/examples/playground/app/a0/server.ts +5 -0
  87. package/examples/playground/app/a0/shell.tsx +1 -1
  88. package/examples/playground/app/a0/uploads/route.test.ts +59 -0
  89. package/examples/playground/app/a0/uploads/route.ts +29 -0
  90. package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
  91. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -89
  92. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
  93. package/examples/playground/app/agent/ingress.test.ts +41 -17
  94. package/examples/playground/app/agent/model-input.test.tsx +92 -43
  95. package/examples/playground/app/agent/model.test.ts +12 -3
  96. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
  97. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
  98. package/examples/playground/app/chat/server.test.ts +38 -65
  99. package/examples/playground/app/components/activity-history.test.tsx +60 -18
  100. package/examples/playground/app/components/fast-link.test.tsx +3 -5
  101. package/examples/playground/app/initial-state.test.tsx +86 -45
  102. package/examples/playground/next.config.ts +7 -4
  103. package/examples/playground/package.json +4 -1
  104. package/examples/playground/test/agent.ts +35 -0
  105. package/examples/playground/test/model.ts +149 -0
  106. package/examples/playground/test/serve.ts +184 -0
  107. package/examples/playground/test/server-only.ts +1 -0
  108. package/examples/playground/vitest.config.ts +27 -0
  109. package/package.json +3 -1
  110. package/sql/postgres/notifications.sql +24 -0
  111. package/sql/postgres/schema.sql +137 -0
  112. package/src/ai-agent.ts +9 -4
  113. package/src/ai-client-state.ts +21 -9
  114. package/src/ai-context-schema.ts +28 -4
  115. package/src/ai-context.ts +83 -3
  116. package/src/ai-control-server.ts +70 -43
  117. package/src/ai-control-state.ts +3 -4
  118. package/src/ai-control.ts +59 -10
  119. package/src/ai-coordinator.ts +7 -6
  120. package/src/ai-fx.ts +95 -23
  121. package/src/ai-harness.ts +171 -29
  122. package/src/ai-message-shape.ts +41 -0
  123. package/src/ai-pi.ts +88 -32
  124. package/src/ai-stored-state.ts +1 -3
  125. package/src/ai.ts +116 -77
  126. package/src/errors.ts +1 -1
  127. package/src/internal.ts +2 -1
  128. package/src/libfx.d.ts +8 -1
  129. package/src/postgres-notifications.ts +0 -22
  130. package/src/postgres-resources.ts +6 -9
  131. package/src/postgres-schema.ts +80 -0
  132. package/src/scheduler-qstash.ts +12 -5
  133. package/src/scheduler-vercel.ts +11 -16
  134. package/src/server.ts +66 -32
  135. package/src/store-postgres.ts +5 -123
  136. package/dist/ai-BMLlI2Ff.d.ts.map +0 -1
  137. package/dist/ai-D6mGlO8j.js.map +0 -1
  138. package/dist/ai-harness-B2suP2Qh.d.ts.map +0 -1
  139. package/dist/ai-harness-BuLh_EYi.js.map +0 -1
  140. package/dist/server-D-Y4pqwu.js.map +0 -1
  141. package/docs/reference/05-adapters.mdx +0 -351
  142. package/docs/server/05-production.mdx +0 -323
  143. package/examples/playground/app/a0/no-photon.cjs +0 -4
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The Postgres schema contract. A2 never runs DDL in a Postgres database:
3
+ * `sql/postgres/schema.sql` (and `notifications.sql` for
4
+ * `listenConnectionString`) create the tables, and a store checks the
5
+ * recorded version before its first query.
6
+ */
7
+
8
+ import { A2Error } from './errors.ts'
9
+ import type { PostgresClient } from './store-postgres.ts'
10
+
11
+ /** The `a2_schema` version this release needs; `schema.sql` records it. */
12
+ export const POSTGRES_SCHEMA_VERSION = 1
13
+
14
+ const PACKAGE = 'node_modules/experimental-a2'
15
+
16
+ const apply = (file: string): string =>
17
+ `apply ${PACKAGE}/sql/postgres/${file} to this database`
18
+
19
+ const SEE_DOCS = `See ${PACKAGE}/docs/stores/04-postgres.mdx.`
20
+
21
+ const VERSION = 'select version from a2_schema'
22
+ const VERSION_AND_TRIGGERS = `select version, (select count(*) from pg_trigger
23
+ where (tgname = 'a2_notify_events' and tgrelid = to_regclass('a2_events'))
24
+ or (tgname = 'a2_notify_presence' and tgrelid = to_regclass('a2_presence')))
25
+ as notify_triggers from a2_schema`
26
+
27
+ /** Explains a failed version read, or returns undefined when the schema is not the cause. */
28
+ const diagnose = async (
29
+ client: PostgresClient,
30
+ ): Promise<A2Error | undefined> => {
31
+ const [found] = (
32
+ await client.query(`select
33
+ to_regclass('a2_schema') is not null as marked,
34
+ to_regclass('a2_events') is not null as installed,
35
+ has_table_privilege(to_regclass('a2_schema'), 'select') as readable`)
36
+ ).rows
37
+ if (!found) return undefined
38
+ if (!found['marked'])
39
+ return new A2Error(
40
+ 'STORE_NOT_CONFIGURED',
41
+ found['installed']
42
+ ? `This Postgres database has A2's tables but no a2_schema version. That is expected once after upgrading from a release that created its tables at runtime, and your events are untouched. A2 doesn't create or change tables at runtime: ${apply('schema.sql')}. ${SEE_DOCS}`
43
+ : `A2's tables aren't in this Postgres database, and A2 doesn't create tables at runtime: ${apply('schema.sql')}. ${SEE_DOCS}`,
44
+ )
45
+ if (found['readable'] === false)
46
+ return new A2Error(
47
+ 'STORE_NOT_CONFIGURED',
48
+ `The role this store connects as can't read a2_schema. Apply schema.sql as that role, or grant it select on a2_schema and select, insert, update, and delete on A2's other tables. ${SEE_DOCS}`,
49
+ )
50
+ return undefined
51
+ }
52
+
53
+ /** Reads the schema version (and, with a listener, the triggers) in one query that takes no lock a writer holds. */
54
+ export const checkPostgresSchema = async ({
55
+ client,
56
+ listen,
57
+ }: {
58
+ client: PostgresClient
59
+ listen: boolean
60
+ }): Promise<void> => {
61
+ let row: Record<string, unknown> | undefined
62
+ try {
63
+ row = (await client.query(listen ? VERSION_AND_TRIGGERS : VERSION)).rows[0]
64
+ } catch (err) {
65
+ throw (await diagnose(client).catch(() => undefined)) ?? err
66
+ }
67
+ const version = Number(row?.['version'] ?? 0)
68
+ if (version < POSTGRES_SCHEMA_VERSION) {
69
+ throw new A2Error(
70
+ 'STORE_NOT_CONFIGURED',
71
+ `This Postgres database has A2 schema version ${version}, and this release of experimental-a2 needs version ${POSTGRES_SCHEMA_VERSION}: ${apply('schema.sql')}. ${SEE_DOCS}`,
72
+ )
73
+ }
74
+ if (listen && Number(row?.['notify_triggers']) < 2) {
75
+ throw new A2Error(
76
+ 'STORE_NOT_CONFIGURED',
77
+ `listenConnectionString needs A2's notification triggers, which aren't in this Postgres database. After schema.sql, ${apply('notifications.sql')}. ${SEE_DOCS}`,
78
+ )
79
+ }
80
+ }
@@ -497,8 +497,11 @@ const readBoundedBody = async (
497
497
  return new TextDecoder().decode(bytes)
498
498
  }
499
499
 
500
- const storeUnavailable = (error: unknown): error is A2Error =>
501
- error instanceof A2Error && error.code === 'STORE_UNAVAILABLE'
500
+ // A store that isn't set up yet (Postgres before schema.sql, a missing driver)
501
+ // is fixed without changing the task, so its work waits like any outage.
502
+ const storeOutage = (error: unknown): error is A2Error =>
503
+ error instanceof A2Error &&
504
+ (error.code === 'STORE_UNAVAILABLE' || error.code === 'STORE_NOT_CONFIGURED')
502
505
 
503
506
  const response = {
504
507
  ok: (): Response => new Response('ok'),
@@ -888,7 +891,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
888
891
  await deliverSchedulerAppend(server, task)
889
892
  else await deliverSchedulerClear(server, task)
890
893
  } catch (error) {
891
- if (!storeUnavailable(error)) throw error
894
+ if (!storeOutage(error)) throw error
892
895
  await sendStoreOutageSuccessor(task, outage)
893
896
  }
894
897
  return response.ok()
@@ -906,13 +909,17 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
906
909
  outcome: 'unknown' as const,
907
910
  }
908
911
  } catch (error) {
909
- if (!storeUnavailable(error)) throw error
912
+ if (!storeOutage(error)) throw error
910
913
  await sendStoreOutageSuccessor(task, outage)
911
914
  return response.ok()
912
915
  }
913
916
  if (result.settled) return response.ok()
914
917
 
915
- if (result.outcome === 'busy' || result.outcome === 'stalled') {
918
+ if (
919
+ result.outcome === 'busy' ||
920
+ result.outcome === 'stalled' ||
921
+ result.outcome === 'failed'
922
+ ) {
916
923
  if (result.schedulerArm) await result.schedulerArm
917
924
  else {
918
925
  await sendTask({
@@ -14,7 +14,8 @@
14
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
- * contention and handler failure arm a later watchdog before acking.
17
+ * contention, handler failure, and a drain that throws after arming its
18
+ * successor all ack once a later watchdog is durable.
18
19
  *
19
20
  * `@vercel/queue` is an optional peer dependency, imported lazily. The
20
21
  * `transport` option is the injection seam — tests run a real little
@@ -100,12 +101,9 @@ export type VercelQueuesOptions = {
100
101
  /** Vercel Queues rejects shorter visibility changes. */
101
102
  const MIN_VISIBILITY_SECONDS = 30
102
103
 
103
- /** Vercel Queues' maximum message retention and delivery delay. */
104
+ /** Vercel Queues' maximum message retention; its default is a day. */
104
105
  const MAX_RETENTION_SECONDS = 7 * 24 * 60 * 60
105
106
 
106
- /** Leave the final retention day for delivery and retries. */
107
- const MAX_DELAY_SECONDS = 6 * 24 * 60 * 60
108
-
109
107
  const TOPIC_PATTERN = /^[A-Za-z0-9_-]+$/
110
108
  const RETRYABLE_HTTP_CLIENT_STATUSES = new Set([408, 409, 425, 429])
111
109
  /** The SDK's send path maps these statuses to name-only error instances. */
@@ -257,14 +255,6 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
257
255
  0,
258
256
  Math.ceil((deliveryTargetAt - nowMs) / 1_000),
259
257
  )
260
- if (deliveryDelaySeconds > MAX_DELAY_SECONDS) {
261
- throw classifySchedulerSendFailure(
262
- new RangeError(
263
- 'vercelQueues() tasks cannot be delayed more than 6 days',
264
- ),
265
- 'terminal',
266
- )
267
- }
268
258
  const message: SchedulerTask = { ...task, dueAt }
269
259
  const idempotencyKey =
270
260
  task.kind === 'drain'
@@ -353,9 +343,14 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
353
343
  }
354
344
  if (result.settled) return
355
345
 
356
- if (result.outcome === 'busy' || result.outcome === 'stalled') {
357
- // The live owner arms its own claim windows, and the store holds
358
- // a failed event until its retry time. Ensure a later slot here
346
+ if (
347
+ result.outcome === 'busy' ||
348
+ result.outcome === 'stalled' ||
349
+ result.outcome === 'failed'
350
+ ) {
351
+ // The live owner arms its own claim windows, the store holds
352
+ // a failed event until its retry time, and a drain that failed
353
+ // after claiming armed its successor. Ensure a later slot here
359
354
  // as well before relinquishing this durable delivery. If the queue is down, the
360
355
  // throw leaves the current message unacked for visibility-timeout
361
356
  // redelivery.
package/src/server.ts CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  } from './stream-buffer.ts'
51
51
  import type {
52
52
  A2Store,
53
+ CompleteAttemptResult,
53
54
  AppendEvent,
54
55
  Event,
55
56
  EventCause,
@@ -1004,7 +1005,7 @@ export function createServer<
1004
1005
  const signal = createDrainSignal()
1005
1006
  const p = new Promise<void>((resolve) => {
1006
1007
  queueMicrotask(() => {
1007
- void drainSession(sessionId, watchdogDueAt, signal)
1008
+ void drainSession(sessionId, { watchdogDueAt, signal })
1008
1009
  .then(() => resolve())
1009
1010
  .catch(() => resolve())
1010
1011
  })
@@ -2931,8 +2932,16 @@ export function createServer<
2931
2932
 
2932
2933
  const drainSession = (
2933
2934
  sessionId: string,
2934
- watchdogDueAt?: number,
2935
- signal?: DrainSignal,
2935
+ {
2936
+ watchdogDueAt,
2937
+ signal,
2938
+ handOffFailure = false,
2939
+ }: {
2940
+ watchdogDueAt?: number | undefined
2941
+ signal?: DrainSignal | undefined
2942
+ /** Return a failure after arming as `failed` instead of throwing it. */
2943
+ handOffFailure?: boolean
2944
+ } = {},
2936
2945
  ): Promise<DrainResult> =>
2937
2946
  telemetry.span(
2938
2947
  'a2.drain',
@@ -3131,7 +3140,8 @@ export function createServer<
3131
3140
  }
3132
3141
  | undefined
3133
3142
  let handlerSignal = supersedeSignal
3134
- let returned: ReturnedEvent[]
3143
+ let completion: CompleteAttemptResult
3144
+ let completing = false
3135
3145
  try {
3136
3146
  if (registration?.abortOn) {
3137
3147
  subscription = await subscribeAbort(
@@ -3158,7 +3168,32 @@ export function createServer<
3158
3168
  await subscription.close()
3159
3169
  subscription = undefined
3160
3170
  }
3161
- returned = await returnedEvents(sessionId, event, result)
3171
+ const returned = await returnedEvents(sessionId, event, result)
3172
+ await renewal
3173
+ completing = true
3174
+ completion = await telemetry.span(
3175
+ 'a2.commit',
3176
+ {
3177
+ 'a2.contract': name,
3178
+ 'a2.session_id': sessionId,
3179
+ 'a2.commit.operation': 'complete',
3180
+ 'a2.commit.index': event.index,
3181
+ 'a2.commit.attempt': event.attemptCount,
3182
+ },
3183
+ async (commitSpan) => {
3184
+ const committed = await store.completeAttempt({
3185
+ sessionId: ns,
3186
+ index: event.index,
3187
+ attempt: event.attemptCount,
3188
+ events: returned,
3189
+ })
3190
+ commitSpan.setAttribute(
3191
+ 'a2.commit.outcome',
3192
+ committed.outcome,
3193
+ )
3194
+ return committed
3195
+ },
3196
+ )
3162
3197
  } catch (err) {
3163
3198
  if (supersedeSignal.aborted && err === supersedeSignal.reason) {
3164
3199
  // The handler surrendered to its own abort — identity, not
@@ -3172,6 +3207,14 @@ export function createServer<
3172
3207
  appendsEvents: false,
3173
3208
  }
3174
3209
  }
3210
+ const refused =
3211
+ completing &&
3212
+ err instanceof A2Error &&
3213
+ err.code === 'PARTIAL_DUPLICATE_BATCH'
3214
+ // A store that cannot record the completion is not the
3215
+ // handler failing: the claim lapses, and the claim that takes
3216
+ // it over counts it. A returned batch it refuses is.
3217
+ if (completing && !refused) throw err
3175
3218
  // Everything else — including a store fence rejection — is
3176
3219
  // adjudicated by failAttempt, which is itself fenced: a
3177
3220
  // genuinely superseded attempt gets 'superseded' back with
@@ -3192,11 +3235,14 @@ export function createServer<
3192
3235
  index: event.index,
3193
3236
  attempt: event.attemptCount,
3194
3237
  error: describeError(err),
3195
- // A NonRetriableError is terminal on its first throw:
3196
- // deterministic failures should not burn the retry
3197
- // budget before dead-lettering.
3238
+ // A NonRetriableError is terminal on its first throw,
3239
+ // and so is a refused batch, whose retry returns the
3240
+ // same ids: deterministic failures should not burn the
3241
+ // retry budget before dead-lettering.
3198
3242
  maxFailures:
3199
- err instanceof NonRetriableError ? 1 : MAX_FAILURES,
3243
+ err instanceof NonRetriableError || refused
3244
+ ? 1
3245
+ : MAX_FAILURES,
3200
3246
  retryAfterMs: failureRetryDelayMs(event.failureCount + 1),
3201
3247
  })
3202
3248
  commitSpan.setAttribute('a2.commit.outcome', result.outcome)
@@ -3212,27 +3258,6 @@ export function createServer<
3212
3258
  } finally {
3213
3259
  await subscription?.close()
3214
3260
  }
3215
- await renewal
3216
- const completion = await telemetry.span(
3217
- 'a2.commit',
3218
- {
3219
- 'a2.contract': name,
3220
- 'a2.session_id': sessionId,
3221
- 'a2.commit.operation': 'complete',
3222
- 'a2.commit.index': event.index,
3223
- 'a2.commit.attempt': event.attemptCount,
3224
- },
3225
- async (commitSpan) => {
3226
- const result = await store.completeAttempt({
3227
- sessionId: ns,
3228
- index: event.index,
3229
- attempt: event.attemptCount,
3230
- events: returned,
3231
- })
3232
- commitSpan.setAttribute('a2.commit.outcome', result.outcome)
3233
- return result
3234
- },
3235
- )
3236
3261
  eventSpan.setAttribute(
3237
3262
  'a2.event.outcome',
3238
3263
  completion.outcome === 'completed'
@@ -3252,7 +3277,7 @@ export function createServer<
3252
3277
  return {
3253
3278
  status: 'processed',
3254
3279
  opensLane: event.lane !== null,
3255
- appendsEvents: returned.length > 0,
3280
+ appendsEvents: completion.events.length > 0,
3256
3281
  }
3257
3282
  },
3258
3283
  )
@@ -3424,6 +3449,12 @@ export function createServer<
3424
3449
  if (signal) signal.closed = true
3425
3450
  break
3426
3451
  }
3452
+ } catch (error) {
3453
+ // The successor armed for this drain's claims drains the session
3454
+ // again, so the scheduler delivery need not stay as a second retry.
3455
+ if (!handOffFailure || schedulerArm === undefined) throw error
3456
+ span.recordError(error)
3457
+ outcome = 'failed'
3427
3458
  } finally {
3428
3459
  if (signal) signal.closed = true
3429
3460
  stopRenewing = true
@@ -3525,7 +3556,10 @@ export function createServer<
3525
3556
  },
3526
3557
  schedulerDrain: (sessionId, opts) => {
3527
3558
  assertSessionId(sessionId)
3528
- return drainSession(sessionId, opts?.watchdogDueAt)
3559
+ return drainSession(sessionId, {
3560
+ watchdogDueAt: opts?.watchdogDueAt,
3561
+ handOffFailure: true,
3562
+ })
3529
3563
  },
3530
3564
  schedulerAppend: async (sessionId, events, originIndex) => {
3531
3565
  if (!scheduler) {
@@ -27,9 +27,9 @@ import { pollPresence } from './store-presence-polling.ts'
27
27
  import {
28
28
  postgresNotifications,
29
29
  postgresNotificationChannel,
30
- POSTGRES_NOTIFICATIONS_SQL,
31
30
  type PostgresListenerConnection,
32
31
  } from './postgres-notifications.ts'
32
+ import { checkPostgresSchema } from './postgres-schema.ts'
33
33
  import { retryableLazy } from './retryable-lazy.ts'
34
34
  import { postgresNotificationScope } from './postgres-notification-scope.ts'
35
35
  import { postgresPoolClient } from './postgres-pool.ts'
@@ -92,91 +92,6 @@ export type PostgresStore = A2Store & {
92
92
  close(): Promise<void>
93
93
  }
94
94
 
95
- const SCHEMA = `
96
- create table if not exists a2_events (
97
- session_id text not null,
98
- idx bigint not null,
99
- event_type text not null,
100
- payload jsonb not null,
101
- event_id text not null,
102
- created_at bigint not null,
103
- cause jsonb,
104
- lane text,
105
- lane_ready boolean not null default false,
106
- processed_at bigint,
107
- processed_by_attempt integer,
108
- returned_event_ids jsonb,
109
- first_claimed_at bigint,
110
- last_claimed_at bigint,
111
- attempt_count integer not null default 0,
112
- failure_count integer not null default 0,
113
- last_failed_at bigint,
114
- last_failed_attempt integer,
115
- last_error text,
116
- failed_at bigint,
117
- claim_holder text,
118
- claim_expires_at bigint,
119
- primary key (session_id, idx)
120
- );
121
-
122
- create unique index if not exists a2_events_event_id on a2_events (event_id);
123
- create index if not exists a2_events_unprocessed
124
- on a2_events (session_id, idx) where processed_at is null;
125
- create index if not exists a2_events_lane_pending
126
- on a2_events (session_id, lane, idx) where processed_at is null;
127
- create index if not exists a2_events_dispatch_ready
128
- on a2_events (session_id, idx)
129
- where lane_ready and processed_at is null and failed_at is null;
130
- create index if not exists a2_events_claim_expiry
131
- on a2_events (session_id, claim_expires_at)
132
- where lane_ready and processed_at is null and failed_at is null;
133
-
134
- create table if not exists a2_snapshots (
135
- session_id text not null,
136
- reducer_name text not null,
137
- up_to_index bigint not null,
138
- state jsonb not null,
139
- updated_at bigint not null,
140
- primary key (session_id, reducer_name)
141
- );
142
-
143
- create table if not exists a2_snapshot_history (
144
- session_id text not null,
145
- reducer_name text not null,
146
- up_to_index bigint not null,
147
- state jsonb not null,
148
- updated_at bigint not null,
149
- primary key (session_id, reducer_name, up_to_index)
150
- );
151
-
152
- create table if not exists a2_snapshot_pins (
153
- session_id text not null,
154
- event_index bigint not null,
155
- reducer_name text not null,
156
- up_to_index bigint not null,
157
- primary key (session_id, event_index, reducer_name, up_to_index)
158
- );
159
-
160
- create index if not exists a2_snapshot_pins_checkpoint
161
- on a2_snapshot_pins (session_id, reducer_name, up_to_index);
162
-
163
- create table if not exists a2_clears (
164
- session_id text primary key,
165
- clear_index bigint not null
166
- );
167
-
168
- create table if not exists a2_presence (
169
- ns text not null,
170
- participant text not null,
171
- field text not null,
172
- value jsonb not null,
173
- seen bigint not null,
174
- at bigint not null,
175
- expires_at bigint not null,
176
- primary key (ns, participant, field)
177
- );
178
- `
179
-
180
95
  const asNumber = (value: unknown): number => Number(value) // bigint columns arrive as strings
181
96
 
182
97
  /**
@@ -281,37 +196,6 @@ const wrap = async <T>(fn: () => Promise<T>): Promise<T> => {
281
196
 
282
197
  const jsonb = (value: unknown): string => JSON.stringify(value) ?? 'null'
283
198
 
284
- const initializeSchema = async ({
285
- client,
286
- listen,
287
- }: {
288
- client: PostgresClient
289
- listen: boolean
290
- }): Promise<void> => {
291
- const schema = SCHEMA + (listen ? POSTGRES_NOTIFICATIONS_SQL : '')
292
- if (!client.connect) {
293
- await client.query(schema)
294
- return
295
- }
296
-
297
- const conn = await client.connect()
298
- try {
299
- await conn.query('begin')
300
- // `if not exists` does not serialize concurrent catalog writes. Every
301
- // serverless isolate takes the same transaction-scoped lock before DDL.
302
- await conn.query(
303
- "select pg_advisory_xact_lock(hashtext('a2:postgres:schema'))",
304
- )
305
- await conn.query(schema)
306
- await conn.query('commit')
307
- } catch (err) {
308
- await conn.query('rollback').catch(() => {})
309
- throw err
310
- } finally {
311
- conn.release()
312
- }
313
- }
314
-
315
199
  const loadPg = async () =>
316
200
  import('pg').catch(() => {
317
201
  throw new A2Error(
@@ -420,12 +304,10 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
420
304
  try {
421
305
  const listen = Boolean(options.listenConnectionString)
422
306
  const resource = lease.value
423
- if (injected) await initializeSchema({ client: resource.client, listen })
424
- else
425
- await resource.initialize({
426
- schema: SCHEMA + (listen ? POSTGRES_NOTIFICATIONS_SQL : ''),
427
- run: () => initializeSchema({ client: resource.client, listen }),
428
- })
307
+ await resource.initialize({
308
+ key: listen ? 'schema+triggers' : 'schema',
309
+ run: () => checkPostgresSchema({ client: resource.client, listen }),
310
+ })
429
311
  return lease
430
312
  } catch (error) {
431
313
  if (!injected) await lease.release().catch(() => {})
@@ -1 +0,0 @@
1
- {"version":3,"file":"ai-BMLlI2Ff.d.ts","names":[],"sources":["../src/ai-message-base.ts","../src/ai-control.ts","../src/ai-client-state.ts","../src/ai-projector.ts","../src/ai-progress-batches.ts","../src/ai-message-projection.ts","../src/ai.ts"],"mappings":";;;;KAGK;EAAc;;EAAsB;;EAAmB;;KACvD,aAAa,eAAe;KAC5B;EAAa,OAAO;;EAAiC,QAAQ;;KAEtD;EACV;EACA,SAAS;EACT,QAAQ,eAAe;;;;KCIb,UAAU,UAAU,YAAY;EAC1C;EACA,SAAS;EACT;EACA;;KAGU,eAAe,UAAU,YAAY;EAC3C;EAAgB,SAAS;EAAG;;EAC5B;EAAgB,SAAS;;EACzB;EAAgB;EAAiB;;EACjC;EAAkB;;EAElB;EACA;EACA;EACA;;EAEA;EAAgB;EAAgB;;EAChC;EAAiB;EAAgB,SAAS;EAAG;;EAC7C;EAAiB;;EACjB;;EACA;EAAuB,QAAQ;;EAC/B;EAAoB,UAAU;;EAC9B;EAAiB,UAAU;;EAC3B;EAAyB,SAAS;;EAClC;EAAiB,SAAS;;EAC1B;EAAqB,SAAS;;KAExB;EACV;EACA;EACA;EACA;;KAUU,WAAW,UAAU,YAAY;EAC3C;EACA,OAAO,UAAU;EACjB;;KA+BU;EAAa;EAAc;EAAY;;KACvC;EACV;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;KAGE;EACV;EACA;EACA,MAAM;EACN,YAAY;;KAYF;EACV;EACA;EACA;EACA;IAAW;IAAe;IAAqB;;EAC/C;IAAS;IAAc;;;KAGb;EACV,SAAS;EACT;IACE;IACA;IACA,OAAO;IACP;IACA;IACA;IACA,eAAe;IACf;IACA;;;;;KCjIQ,aAAa,UAAU,aAAa,QAC9C,eAAe;EAEb;;KAYQ,oBAAoB,UAAU;EACxC;EACA,SAAS,aAAa;;;;KCfnB;EACH;EACA;EACA;EACA;EACA;EACA;EACA;;KAwJU;EACV,MAAM;EACN,WAAW;EACX,OAAO,eAAe;;;;KChJZ;EAAkB;EAAe,QAAQ;;KAChD,aAAa;EAAoB,MAAM;EAAY,OAAO;;KAEnD;EACV;EACA,MAAM;EACN,QAAQ,MAAM;;;;cCmgBH,mBAAoB;EAC/B;MACE,QAAQ;;;KCtcA;KAGA;KAEA,iBAAiB,YAAY;cAE5B,0BAA0B;KAU3B;EACV;EACA;EACA;EACA;EACA,QAAQ;;;KAIE;EACV;EACA;EACA,MAAM;;;;;;KASI,kBAAkB,UAAU,eAAa,KAAK;EACxD,OAAO,MAAM,qBAAqB;;KAGxB;EAA0B,WAAW;;KACrC;EAAyB;;KACzB,sBAAsB,UAAU;EAC1C,SAAS;;EAET;;KAEU;EAA4B;;KACnC;EACH;EACA;;EAEA;;KAEU,4BAA4B;EAEhC;EAAsB;;EACtB;EAAmB;;KAEf;EACV;IAAY;IAAgB;;EAC5B;EACA,QAAQ;EACR;EACA;;KAEU;EACV;EACA;EACA;;KAEU,wBAAwB;KACxB;EACV;EACA;EACA;EACA;EACA;;EAEA;EACA;;KAEU;;EAEV;EACA,QAAQ;;KAEE;EACV;EACA;EACA;EACA;EACA,eAAe;EACf,QAAQ;EACR;;KAEU;EACV;EACA;EACA;EACA;EACA;;EAEA;EACA;;KAEU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;KAEU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;KAEU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;;KAEU;EACV;EACA;EACA;EACA;EACA;EACA;;KAEU;EACV;EACA;EACA;EACA;EACA;EACA,WAAW;;KAED;EACV;EACA;EACA;EACA;EACA;EACA,OAAO;;KAEG;EACV;EACA;;KAEU;EAAkC;;KAClC;EACV;EACA,QAAQ;;KAEE;EACN;;EACA;EAAoB,QAAQ;;EAC5B;;KAEM;EACV;EACA;EACA;EACA;EACA;EACA;;KAEU,2BAA2B,UAAU;EAC/C;EACA;EACA;EACA,UAAU;EACV;EACA;EACA,QAAQ;;KAGE,YAAY,UAAU,cAAY;EAC5C,wBAAwB,iBAAiB,eAAe;EACxD,wBAAwB,iBAAiB;EACzC,sBAAsB,iBAAiB;EACvC,oBAAoB,iBAAiB;EACrC,+BAA+B,iBAAiB;EAChD,wBAAwB;IACtB;IACA;;EAEF,sBAAsB,iBAAiB;EACvC,qBAAqB,iBAAiB;EACtC,sBAAsB,iBAAiB,sBAAsB;EAC7D,wBAAwB,iBAAiB;EACzC,0BAA0B,iBAAiB;EAC3C,2BAA2B,iBAAiB;EAC5C,sBAAsB,iBAAiB;EACvC,yBAAyB,iBAAiB;EAC1C,0BAA0B,iBAAiB;EAC3C,2BAA2B,iBAAiB;EAC5C,wBAAwB,iBAAiB;EACzC,kBAAkB,iBAAiB;EACnC,kBAAkB,iBAAiB;EACnC,yBAAyB,iBAAiB;EAC1C,yBAAyB,iBAAiB;EAC1C,sBAAsB,iBAAiB;EACvC,sBAAsB,iBAAiB;EACvC,2BAA2B,iBAAiB;EAC5C,2BAA2B,iBAAiB,2BAA2B;EACvE,+BAA+B,iBAAiB;EAChD,8BAA8B,iBAAiB;;KAGrC,mBAAmB;;EAE7B;;KAGU;EACV;EACA;;EAEA;EACA,OAAO;EACP;EACA;IAAW;IAAsB,QAAQ;;EACzC,iBAAiB;EACjB,SAAS;EACT,WAAW;EACX,YAAY;IACN;IAAe;IAAwB,SAAS;;IAChD;IAAe;IAAwB,SAAS;;EAEtD,gBAAgB;IAEV;IACA;IACA,SAAS;;IAGT;IACA;IACA,SAAS;;;KAKL,kBAAkB;KAClB,eAAe;KAEf;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA,QAAQ;;KAGE;EACV;EACA,OAAO;;KAGG,QAAQ,UAAU,cAAY;EACxC;IAAS;IAAiB,OAAO,UAAU;;EAC3C,uBAAuB,oBAAoB;EAC3C,QAAQ,WAAW;EACnB;EACA,SAAS;EACT,sBAAsB;EACtB;IAAW;IAAkB,WAAW;;EACxC,UAAU,kBAAkB;EAC5B,QAAQ;EACR,kBAAkB;EAClB;EACA,qBAAqB;EACrB;EACA;EACA,kBAAkB;EAClB,gBAAgB,eAEd,KAAK;EAEP,qBAAqB;EACrB,sBAAsB;EAItB,kBAAkB;EAClB,eAAe;EACf,OAAO;EACP,YAAY;EACZ,eAAe,eAAe;EAC9B,OAAO;EACP;;;KAIU,YAAY,KAAK;WAClB,4BAA4B,UAAU;IAE7C,iBAAiB,YAAY,WAAW,4BAChC,UAAU,eAEhB,IACA,cACF;;KAGQ,cACV,UAAU,cAAY,aACtB,UAAU,YAAY,YAAY,IAClC,mBAAmB,wBACjB,SAAS,GAAG,sBAAsB,aAAa,GAAG,GAAG;KAE7C,qBAAqB,YAC/B,KAAK;;KAGK,iBAAiB,UAAU,sBAAsB,eAC3D;;;KAMG,QAAQ,QAAQ,KAAK,cAAc,SAAS;;KAE5C,SAAS,WAAW;;;;;;;KAQb,aACV,UAAU,aACV,UAAU,WACV,2BACS,kBACP,QAAQ,GAAG,QAAQ;WAAiB,aAAa;IACjD,QAAQ,GAAG,QAAQ,KAAK;WAAiB,aAAa;;KAE9C,qBACV,UAAU,aACV,UAAU,WACV;EAEA;EACA,SAAS;;EAET,gBAAgB,iBAAiB;;;;;EAKjC;;IAEE;IACA,cAAc,IAAI,QAAQ,KAAK,SAAS;IACxC,OACE,OAAO,QAAQ,IACf,OAAO,cAAc,YAAY,KAAK,QAAQ,QAC3C,QAAQ;;;iBA6MD,aAAa,UAAU,cAAY,aAAW;EAC5D,gBAAgB,iBAAiB;IAC/B,YAAY;cAsUH,QAAQ,YAAY;;iBA2GjB,cAAc,UAAU,aACtC,OAAO,QAAQ,IACf;EAAS;EAAc;EAAkB;EAAe;IACvD,QAAQ;;iBAmrCK,iBAAiB,UAAU,cAAY,aACrD;EAAoB;EAAc;EAAkB;MACnD,kBAAkB;KAMhB,aAAa;EAAO;EAA8B,SAAS;EAAG;;KAC9D,aACH,UAAU,aACV,UAAU,eAAe,gBACvB,aAAa,QAAQ,eAAe;EAAM,QAAQ;OACnD;EAA+B;;KAEtB;EACV,QAAQ,UAAU,aAChB,SAAS,GACT;IAAY;MACX,aAAa;EAChB,KAAK,UAAU,aAAW,SAAS,IAAI,aAAa;EACpD;IACE,OAAO;MAAW;QAAoB,aAAa;IACnD,QAAQ;MACN;MACA;MACA;QACE,aAAa;IACjB,KAAK,UAAU,aAAW;MACxB,SAAS;QACP,aAAa;IACjB,KAAK;MACH;MACA;QACE,aAAa;;EAEnB,KAAK;IACH;IACA;MACE,aAAa;EACjB,MAAM,UAAU,aAAW;IACzB;IACA,SAAS;IACT;MACE,aAAa;EACjB,MAAM;IACJ;MACE,aAAa;EACjB,UAAU,aAAa;EACvB,SACE,UAAU,2BACT,aAAa;EAChB,MAAM,UAAU,wBAAwB,aAAa;EACrD,aACE,SAAS,wBACR,aAAa;EAChB,MAAM,SAAS,yBAAyB,aAAa;EACrD,UACE,SAAS,4BACR,aAAa;EAChB,WACE,QAAQ,oBACP,aAAa;;;cAsBL,QAAQ;;;;;;;;;iBAgEL,eAAe,UAAU,qBAAqB,aAC5D,WAAW,SAAS,KACnB,iBAAiB;;cAoBP;;;;;;;iBAQG,cACd,UAAU,cAAY,aACtB,UAAU,YAAY,sBACtB,mBAAmB,sBAEnB,SAAS,qBAAqB,GAAG,GAAG,KACnC,cAAc,GAAG,YAAY,KAAK,GAAG"}