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
package/dist/server.js CHANGED
@@ -1,2 +1,2 @@
1
- import { i as handler, n as deliverSchedulerAppend, r as deliverSchedulerClear, t as createServer } from "./server-D-Y4pqwu.js";
1
+ import { i as handler, n as deliverSchedulerAppend, r as deliverSchedulerClear, t as createServer } from "./server-qyb5aJJG.js";
2
2
  export { createServer, deliverSchedulerAppend, deliverSchedulerClear, handler };
@@ -1 +1 @@
1
- {"version":3,"file":"session-clear-DGI4jTQ5.js","names":[],"sources":["../src/errors.ts","../src/session-clear.ts"],"sourcesContent":["export type A2ServerErrorCode =\n | 'INVALID_PAYLOAD' // schema validation failed — thrown before anything is written\n | 'FORBIDDEN' // external ingress was denied by server.fetch authorization\n | 'UNKNOWN_EVENT_TYPE' // event type not in the machine's `events` map\n | 'PARTIAL_DUPLICATE_BATCH' // batch mixed already-appended and fresh events\n | 'SUPERSEDED_ATTEMPT' // handler append from an attempt a recovery claim or a clear replaced\n | 'SESSION_CLEARED' // a read or call reached across the session's latest clear\n | 'CLAIM_EXPIRED' // ctx.signal abort reason: the attempt's lease lapsed without renewal\n | 'STORE_UNAVAILABLE' // store backend failure — retryable, nothing was written\n | 'STORE_NOT_CONFIGURED' // production boot with no `store` passed\n | 'UNKNOWN_PRESENCE_FIELD' // presence field not in the contract's `presence` map\n | 'PRESENCE_NOT_SUPPORTED' // contract declares presence but the store lacks the capability\n\nexport type A2ErrorCode =\n A2ServerErrorCode | 'TIMEOUT' | 'HTTP_ERROR' | 'CONNECTION_FAILED' | 'UNKNOWN'\n\nexport class A2Error extends Error {\n readonly code: A2ErrorCode\n /** e.g. the Standard Schema issues for INVALID_PAYLOAD */\n readonly details: unknown\n\n constructor(\n code: A2ErrorCode,\n message: string,\n options?: { details?: unknown; cause?: unknown },\n ) {\n super(\n message,\n options && 'cause' in options ? { cause: options.cause } : undefined,\n )\n this.name = 'A2Error'\n this.code = code\n this.details = options?.details\n }\n}\n\n/** A handler write whose attempt no longer owns its triggering event. */\nexport function supersededAttemptError(\n sessionId: string,\n cause: { index: number; attempt: number },\n): A2Error {\n return new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `attempt ${cause.attempt} no longer owns event ${cause.index} in session '${sessionId}'`,\n )\n}\n\n/**\n * Wrap an arbitrary thrown value as STORE_UNAVAILABLE, preserving the\n * original as `cause`. A2Errors pass through untouched — they already\n * carry their own meaning.\n */\nexport function asStoreUnavailable(err: unknown): A2Error {\n if (err instanceof A2Error) return err\n return new A2Error('STORE_UNAVAILABLE', 'the store backend failed', {\n cause: err,\n })\n}\n\n/**\n * Throw from a handler to reject retrying: the failure is adjudicated\n * terminal immediately (dead-lettered on the first throw) instead of\n * consuming the ten-failure retry budget. For deterministic failures —\n * a guard that will refuse identically on every attempt — retrying is\n * waste and dead-lettering is the honest outcome. Matched by\n * `instanceof`, so subclasses carry domain meaning.\n */\nexport class NonRetriableError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'NonRetriableError'\n }\n}\n","import { A2Error } from './errors.ts'\nimport type { Event } from './store.ts'\n\n/** A2's own event types start with this; application contracts cannot. */\nexport const RESERVED_EVENT_PREFIX = 'a2.'\n\n/** Reserved type of the marker a clear appends. */\nexport const CLEAR_EVENT_TYPE = 'a2.cleared'\n\n/** The marker `session.stream()` yields where a clear ended a lifetime. */\nexport type SessionCleared = Event & {\n type: typeof CLEAR_EVENT_TYPE\n payload: null\n}\n\n/** Accepts any stream item, including presence patches and snapshots. */\nexport function isSessionCleared(item: object): item is SessionCleared {\n return (item as { type?: unknown }).type === CLEAR_EVENT_TYPE\n}\n\nexport function sessionClearedError(message: string): A2Error {\n return new A2Error('SESSION_CLEARED', message)\n}\n"],"mappings":";AAgBA,IAAa,UAAb,cAA6B,MAAM;CACjC;;CAEA;CAEA,YACE,MACA,SACA,SACA;EACA,MACE,SACA,WAAW,WAAW,UAAU,EAAE,OAAO,QAAQ,MAAM,IAAI,KAAA,CAC7D;EACA,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,UAAU,SAAS;CAC1B;AACF;;AAGA,SAAgB,uBACd,WACA,OACS;CACT,OAAO,IAAI,QACT,sBACA,WAAW,MAAM,QAAQ,wBAAwB,MAAM,MAAM,eAAe,UAAU,EACxF;AACF;;;;;;AAOA,SAAgB,mBAAmB,KAAuB;CACxD,IAAI,eAAe,SAAS,OAAO;CACnC,OAAO,IAAI,QAAQ,qBAAqB,4BAA4B,EAClE,OAAO,IACT,CAAC;AACH;;;;;;;;;AAUA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,SAAiB,SAA+B;EAC1D,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CACd;AACF;;;;ACjEA,MAAa,mBAAmB;;AAShC,SAAgB,iBAAiB,MAAsC;CACrE,OAAQ,KAA4B,SAAS;AAC/C;AAEA,SAAgB,oBAAoB,SAA0B;CAC5D,OAAO,IAAI,QAAQ,mBAAmB,OAAO;AAC/C"}
1
+ {"version":3,"file":"session-clear-DGI4jTQ5.js","names":[],"sources":["../src/errors.ts","../src/session-clear.ts"],"sourcesContent":["export type A2ServerErrorCode =\n | 'INVALID_PAYLOAD' // schema validation failed — thrown before anything is written\n | 'FORBIDDEN' // external ingress was denied by server.fetch authorization\n | 'UNKNOWN_EVENT_TYPE' // event type not in the machine's `events` map\n | 'PARTIAL_DUPLICATE_BATCH' // batch mixed already-appended and fresh events\n | 'SUPERSEDED_ATTEMPT' // handler append from an attempt a recovery claim or a clear replaced\n | 'SESSION_CLEARED' // a read or call reached across the session's latest clear\n | 'CLAIM_EXPIRED' // ctx.signal abort reason: the attempt's lease lapsed without renewal\n | 'STORE_UNAVAILABLE' // store backend failure — retryable, nothing was written\n | 'STORE_NOT_CONFIGURED' // no `store` in production, a missing peer dependency, or Postgres lacks A2's tables\n | 'UNKNOWN_PRESENCE_FIELD' // presence field not in the contract's `presence` map\n | 'PRESENCE_NOT_SUPPORTED' // contract declares presence but the store lacks the capability\n\nexport type A2ErrorCode =\n A2ServerErrorCode | 'TIMEOUT' | 'HTTP_ERROR' | 'CONNECTION_FAILED' | 'UNKNOWN'\n\nexport class A2Error extends Error {\n readonly code: A2ErrorCode\n /** e.g. the Standard Schema issues for INVALID_PAYLOAD */\n readonly details: unknown\n\n constructor(\n code: A2ErrorCode,\n message: string,\n options?: { details?: unknown; cause?: unknown },\n ) {\n super(\n message,\n options && 'cause' in options ? { cause: options.cause } : undefined,\n )\n this.name = 'A2Error'\n this.code = code\n this.details = options?.details\n }\n}\n\n/** A handler write whose attempt no longer owns its triggering event. */\nexport function supersededAttemptError(\n sessionId: string,\n cause: { index: number; attempt: number },\n): A2Error {\n return new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `attempt ${cause.attempt} no longer owns event ${cause.index} in session '${sessionId}'`,\n )\n}\n\n/**\n * Wrap an arbitrary thrown value as STORE_UNAVAILABLE, preserving the\n * original as `cause`. A2Errors pass through untouched — they already\n * carry their own meaning.\n */\nexport function asStoreUnavailable(err: unknown): A2Error {\n if (err instanceof A2Error) return err\n return new A2Error('STORE_UNAVAILABLE', 'the store backend failed', {\n cause: err,\n })\n}\n\n/**\n * Throw from a handler to reject retrying: the failure is adjudicated\n * terminal immediately (dead-lettered on the first throw) instead of\n * consuming the ten-failure retry budget. For deterministic failures —\n * a guard that will refuse identically on every attempt — retrying is\n * waste and dead-lettering is the honest outcome. Matched by\n * `instanceof`, so subclasses carry domain meaning.\n */\nexport class NonRetriableError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'NonRetriableError'\n }\n}\n","import { A2Error } from './errors.ts'\nimport type { Event } from './store.ts'\n\n/** A2's own event types start with this; application contracts cannot. */\nexport const RESERVED_EVENT_PREFIX = 'a2.'\n\n/** Reserved type of the marker a clear appends. */\nexport const CLEAR_EVENT_TYPE = 'a2.cleared'\n\n/** The marker `session.stream()` yields where a clear ended a lifetime. */\nexport type SessionCleared = Event & {\n type: typeof CLEAR_EVENT_TYPE\n payload: null\n}\n\n/** Accepts any stream item, including presence patches and snapshots. */\nexport function isSessionCleared(item: object): item is SessionCleared {\n return (item as { type?: unknown }).type === CLEAR_EVENT_TYPE\n}\n\nexport function sessionClearedError(message: string): A2Error {\n return new A2Error('SESSION_CLEARED', message)\n}\n"],"mappings":";AAgBA,IAAa,UAAb,cAA6B,MAAM;CACjC;;CAEA;CAEA,YACE,MACA,SACA,SACA;EACA,MACE,SACA,WAAW,WAAW,UAAU,EAAE,OAAO,QAAQ,MAAM,IAAI,KAAA,CAC7D;EACA,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,UAAU,SAAS;CAC1B;AACF;;AAGA,SAAgB,uBACd,WACA,OACS;CACT,OAAO,IAAI,QACT,sBACA,WAAW,MAAM,QAAQ,wBAAwB,MAAM,MAAM,eAAe,UAAU,EACxF;AACF;;;;;;AAOA,SAAgB,mBAAmB,KAAuB;CACxD,IAAI,eAAe,SAAS,OAAO;CACnC,OAAO,IAAI,QAAQ,qBAAqB,4BAA4B,EAClE,OAAO,IACT,CAAC;AACH;;;;;;;;;AAUA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,SAAiB,SAA+B;EAC1D,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CACd;AACF;;;;ACjEA,MAAa,mBAAmB;;AAShC,SAAgB,iBAAiB,MAAsC;CACrE,OAAQ,KAA4B,SAAS;AAC/C;AAEA,SAAgB,oBAAoB,SAA0B;CAC5D,OAAO,IAAI,QAAQ,mBAAmB,OAAO;AAC/C"}
@@ -1 +1 @@
1
- {"version":3,"file":"store-postgres.d.ts","names":[],"sources":["../src/store-postgres.ts"],"mappings":";;;KAuDY;EAAwB,MAAM;;;KAG9B;EACV,MAAM,cAAc,qBAAqB,QAAQ;EACjD;;;;;;;;KASU;EACV,MAAM,cAAc,qBAAqB,QAAQ;EACjD,YAAY,QAAQ;EACpB,QAAQ;;KAGE;;EAEV;;EAEA;;EAEA,SAAS;;EAET,QAAQ;;EAER,MAAM;;KAGI,gBAAgB;;;EAG1B,SAAS;;iBAiSK,SAAS,UAAS,uBAA4B"}
1
+ {"version":3,"file":"store-postgres.d.ts","names":[],"sources":["../src/store-postgres.ts"],"mappings":";;;KAuDY;EAAwB,MAAM;;;KAG9B;EACV,MAAM,cAAc,qBAAqB,QAAQ;EACjD;;;;;;;;KASU;EACV,MAAM,cAAc,qBAAqB,QAAQ;EACjD,YAAY,QAAQ;EACpB,QAAQ;;KAGE;;EAEV;;EAEA;;EAEA,SAAS;;EAET,QAAQ;;EAER,MAAM;;KAGI,gBAAgB;;;EAG1B,SAAS;;iBA6KK,SAAS,UAAS,uBAA4B"}
@@ -171,27 +171,36 @@ function postgresNotifications(createConnection) {
171
171
  }
172
172
  };
173
173
  }
174
- const POSTGRES_NOTIFICATIONS_SQL = `
175
- create or replace function a2_notify_events() returns trigger language plpgsql as $$
176
- begin
177
- perform pg_notify('a2_events_' || md5(new.session_id), '');
178
- return null;
179
- end $$;
180
- create or replace function a2_notify_presence() returns trigger language plpgsql as $$
181
- begin
182
- perform pg_notify('a2_presence_' || md5(case when TG_OP = 'DELETE' then old.ns else new.ns end),
183
- coalesce(current_setting('a2.presence_source', true), ''));
184
- return null;
185
- end $$;
186
- do $$ begin
187
- if not exists (select 1 from pg_trigger where tgname = 'a2_notify_events' and tgrelid = 'a2_events'::regclass) then
188
- create trigger a2_notify_events after insert on a2_events for each row execute function a2_notify_events();
189
- end if;
190
- if not exists (select 1 from pg_trigger where tgname = 'a2_notify_presence' and tgrelid = 'a2_presence'::regclass) then
191
- create trigger a2_notify_presence after insert or update or delete on a2_presence for each row execute function a2_notify_presence();
192
- end if;
193
- end $$;
194
- `;
174
+ const PACKAGE = "node_modules/experimental-a2";
175
+ const apply = (file) => `apply ${PACKAGE}/sql/postgres/${file} to this database`;
176
+ const SEE_DOCS = `See ${PACKAGE}/docs/stores/04-postgres.mdx.`;
177
+ const VERSION = "select version from a2_schema";
178
+ const VERSION_AND_TRIGGERS = `select version, (select count(*) from pg_trigger
179
+ where (tgname = 'a2_notify_events' and tgrelid = to_regclass('a2_events'))
180
+ or (tgname = 'a2_notify_presence' and tgrelid = to_regclass('a2_presence')))
181
+ as notify_triggers from a2_schema`;
182
+ /** Explains a failed version read, or returns undefined when the schema is not the cause. */
183
+ const diagnose = async (client) => {
184
+ const [found] = (await client.query(`select
185
+ to_regclass('a2_schema') is not null as marked,
186
+ to_regclass('a2_events') is not null as installed,
187
+ has_table_privilege(to_regclass('a2_schema'), 'select') as readable`)).rows;
188
+ if (!found) return void 0;
189
+ if (!found["marked"]) return new A2Error("STORE_NOT_CONFIGURED", found["installed"] ? `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}` : `A2's tables aren't in this Postgres database, and A2 doesn't create tables at runtime: ${apply("schema.sql")}. ${SEE_DOCS}`);
190
+ if (found["readable"] === false) return new A2Error("STORE_NOT_CONFIGURED", `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}`);
191
+ };
192
+ /** Reads the schema version (and, with a listener, the triggers) in one query that takes no lock a writer holds. */
193
+ const checkPostgresSchema = async ({ client, listen }) => {
194
+ let row;
195
+ try {
196
+ row = (await client.query(listen ? VERSION_AND_TRIGGERS : VERSION)).rows[0];
197
+ } catch (err) {
198
+ throw await diagnose(client).catch(() => void 0) ?? err;
199
+ }
200
+ const version = Number(row?.["version"] ?? 0);
201
+ if (version < 1) throw new A2Error("STORE_NOT_CONFIGURED", `This Postgres database has A2 schema version ${version}, and this release of experimental-a2 needs version 1: ${apply("schema.sql")}. ${SEE_DOCS}`);
202
+ if (listen && Number(row?.["notify_triggers"]) < 2) throw new A2Error("STORE_NOT_CONFIGURED", `listenConnectionString needs A2's notification triggers, which aren't in this Postgres database. After schema.sql, ${apply("notifications.sql")}. ${SEE_DOCS}`);
203
+ };
195
204
  //#endregion
196
205
  //#region src/postgres-notification-scope.ts
197
206
  const failure = (error) => error instanceof A2Error ? error : new A2Error("STORE_UNAVAILABLE", "Postgres subscription failed", { cause: error });
@@ -446,7 +455,7 @@ function postgresConnectionKey({ connectionString, defaults }) {
446
455
  }
447
456
  const queries = (raw) => {
448
457
  let pending = Promise.resolve();
449
- const schemas = /* @__PURE__ */ new Map();
458
+ const initialized = /* @__PURE__ */ new Map();
450
459
  const exclusive = (fn) => {
451
460
  const result = pending.then(fn);
452
461
  pending = result.catch(() => {});
@@ -456,13 +465,13 @@ const queries = (raw) => {
456
465
  raw,
457
466
  client: raw.connect ? raw : { query: (text, values) => exclusive(() => raw.query(text, values)) },
458
467
  exclusive,
459
- initialize({ schema, run }) {
460
- const existing = schemas.get(schema);
468
+ initialize({ key, run }) {
469
+ const existing = initialized.get(key);
461
470
  if (existing) return existing;
462
471
  const attempt = run();
463
- schemas.set(schema, attempt);
472
+ initialized.set(key, attempt);
464
473
  attempt.catch(() => {
465
- if (schemas.get(schema) === attempt) schemas.delete(schema);
474
+ if (initialized.get(key) === attempt) initialized.delete(key);
466
475
  });
467
476
  return attempt;
468
477
  },
@@ -506,90 +515,6 @@ const sharePostgresListener = ({ key, create }) => acquire({
506
515
  * `pg_advisory_xact_lock`, so concurrent writers can't race the index
507
516
  * assignment.
508
517
  */
509
- const SCHEMA = `
510
- create table if not exists a2_events (
511
- session_id text not null,
512
- idx bigint not null,
513
- event_type text not null,
514
- payload jsonb not null,
515
- event_id text not null,
516
- created_at bigint not null,
517
- cause jsonb,
518
- lane text,
519
- lane_ready boolean not null default false,
520
- processed_at bigint,
521
- processed_by_attempt integer,
522
- returned_event_ids jsonb,
523
- first_claimed_at bigint,
524
- last_claimed_at bigint,
525
- attempt_count integer not null default 0,
526
- failure_count integer not null default 0,
527
- last_failed_at bigint,
528
- last_failed_attempt integer,
529
- last_error text,
530
- failed_at bigint,
531
- claim_holder text,
532
- claim_expires_at bigint,
533
- primary key (session_id, idx)
534
- );
535
-
536
- create unique index if not exists a2_events_event_id on a2_events (event_id);
537
- create index if not exists a2_events_unprocessed
538
- on a2_events (session_id, idx) where processed_at is null;
539
- create index if not exists a2_events_lane_pending
540
- on a2_events (session_id, lane, idx) where processed_at is null;
541
- create index if not exists a2_events_dispatch_ready
542
- on a2_events (session_id, idx)
543
- where lane_ready and processed_at is null and failed_at is null;
544
- create index if not exists a2_events_claim_expiry
545
- on a2_events (session_id, claim_expires_at)
546
- where lane_ready and processed_at is null and failed_at is null;
547
-
548
- create table if not exists a2_snapshots (
549
- session_id text not null,
550
- reducer_name text not null,
551
- up_to_index bigint not null,
552
- state jsonb not null,
553
- updated_at bigint not null,
554
- primary key (session_id, reducer_name)
555
- );
556
-
557
- create table if not exists a2_snapshot_history (
558
- session_id text not null,
559
- reducer_name text not null,
560
- up_to_index bigint not null,
561
- state jsonb not null,
562
- updated_at bigint not null,
563
- primary key (session_id, reducer_name, up_to_index)
564
- );
565
-
566
- create table if not exists a2_snapshot_pins (
567
- session_id text not null,
568
- event_index bigint not null,
569
- reducer_name text not null,
570
- up_to_index bigint not null,
571
- primary key (session_id, event_index, reducer_name, up_to_index)
572
- );
573
-
574
- create index if not exists a2_snapshot_pins_checkpoint
575
- on a2_snapshot_pins (session_id, reducer_name, up_to_index);
576
-
577
- create table if not exists a2_clears (
578
- session_id text primary key,
579
- clear_index bigint not null
580
- );
581
-
582
- create table if not exists a2_presence (
583
- ns text not null,
584
- participant text not null,
585
- field text not null,
586
- value jsonb not null,
587
- seen bigint not null,
588
- at bigint not null,
589
- expires_at bigint not null,
590
- primary key (ns, participant, field)
591
- );
592
- `;
593
518
  const asNumber = (value) => Number(value);
594
519
  /**
595
520
  * The session's latest clear marker, or 0, as a primary-key subquery. Read
@@ -651,25 +576,6 @@ const wrap = async (fn) => {
651
576
  }
652
577
  };
653
578
  const jsonb = (value) => JSON.stringify(value) ?? "null";
654
- const initializeSchema = async ({ client, listen }) => {
655
- const schema = SCHEMA + (listen ? POSTGRES_NOTIFICATIONS_SQL : "");
656
- if (!client.connect) {
657
- await client.query(schema);
658
- return;
659
- }
660
- const conn = await client.connect();
661
- try {
662
- await conn.query("begin");
663
- await conn.query("select pg_advisory_xact_lock(hashtext('a2:postgres:schema'))");
664
- await conn.query(schema);
665
- await conn.query("commit");
666
- } catch (err) {
667
- await conn.query("rollback").catch(() => {});
668
- throw err;
669
- } finally {
670
- conn.release();
671
- }
672
- };
673
579
  const loadPg = async () => import("pg").catch(() => {
674
580
  throw new A2Error("STORE_NOT_CONFIGURED", "Postgres connections created by A2 require the optional 'pg' package");
675
581
  });
@@ -736,13 +642,9 @@ function postgres(options = {}) {
736
642
  try {
737
643
  const listen = Boolean(options.listenConnectionString);
738
644
  const resource = lease.value;
739
- if (injected) await initializeSchema({
740
- client: resource.client,
741
- listen
742
- });
743
- else await resource.initialize({
744
- schema: SCHEMA + (listen ? POSTGRES_NOTIFICATIONS_SQL : ""),
745
- run: () => initializeSchema({
645
+ await resource.initialize({
646
+ key: listen ? "schema+triggers" : "schema",
647
+ run: () => checkPostgresSchema({
746
648
  client: resource.client,
747
649
  listen
748
650
  })