@happyvertical/smrt-core 0.55.6 → 0.55.8

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 (205) hide show
  1. package/AGENTS.md +2 -1
  2. package/agents/browser-entry.md +44 -0
  3. package/agents/build-knowledge.md +20 -0
  4. package/agents/recipes.md +217 -0
  5. package/agents/revision-guard.md +72 -1
  6. package/dist/audit-context.d.ts.map +1 -1
  7. package/dist/audit-context.js +2 -2
  8. package/dist/audit-context.js.map +1 -1
  9. package/dist/browser.d.ts +2 -1
  10. package/dist/browser.d.ts.map +1 -1
  11. package/dist/browser.js +7 -7
  12. package/dist/cascade.d.ts +1 -1
  13. package/dist/cascade.d.ts.map +1 -1
  14. package/dist/cascade.js +1 -1
  15. package/dist/cascade.js.map +1 -1
  16. package/dist/change-feed.d.ts.map +1 -1
  17. package/dist/change-feed.js +1 -2
  18. package/dist/change-feed.js.map +1 -1
  19. package/dist/class.d.ts +1 -1
  20. package/dist/class.d.ts.map +1 -1
  21. package/dist/class.js +7 -3
  22. package/dist/class.js.map +1 -1
  23. package/dist/collection.d.ts.map +1 -1
  24. package/dist/collection.js +1 -1
  25. package/dist/collection.js.map +1 -1
  26. package/dist/consumer-plugin/index.js +2 -2
  27. package/dist/data-query.d.ts.map +1 -1
  28. package/dist/data-query.js +2 -13
  29. package/dist/data-query.js.map +1 -1
  30. package/dist/database.js +1 -1
  31. package/dist/database.js.map +1 -1
  32. package/dist/dispatch/bus.js +1 -1
  33. package/dist/dispatch/bus.js.map +1 -1
  34. package/dist/embedded-write-queue.d.ts +1 -1
  35. package/dist/embedded-write-queue.d.ts.map +1 -1
  36. package/dist/embedded-write-queue.js +7 -5
  37. package/dist/embedded-write-queue.js.map +1 -1
  38. package/dist/embeddings/hash.js +2 -2
  39. package/dist/embeddings/hash.js.map +1 -1
  40. package/dist/embeddings/storage.d.ts.map +1 -1
  41. package/dist/embeddings/storage.js +1 -2
  42. package/dist/embeddings/storage.js.map +1 -1
  43. package/dist/errors.d.ts.map +1 -1
  44. package/dist/errors.js +1 -0
  45. package/dist/errors.js.map +1 -1
  46. package/dist/generation-snapshot.js +1 -1
  47. package/dist/generators/index.js +1 -1
  48. package/dist/generators/mcp.js +3 -3
  49. package/dist/generators/preflight-route.js +1 -1
  50. package/dist/generators/rest.js +1 -1
  51. package/dist/generators.js +1 -1
  52. package/dist/host.browser.d.ts +12 -0
  53. package/dist/host.browser.d.ts.map +1 -0
  54. package/dist/host.browser.js +30 -0
  55. package/dist/host.browser.js.map +1 -0
  56. package/dist/host.d.ts +18 -0
  57. package/dist/host.d.ts.map +1 -0
  58. package/dist/host.js +15 -0
  59. package/dist/host.js.map +1 -0
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +10 -9
  63. package/dist/knowledge.d.ts.map +1 -1
  64. package/dist/knowledge.js +6 -4
  65. package/dist/knowledge.js.map +1 -1
  66. package/dist/manifest/browser-capability.generated.d.ts +14 -0
  67. package/dist/manifest/browser-capability.generated.d.ts.map +1 -0
  68. package/dist/manifest/browser-capability.generated.js +499 -0
  69. package/dist/manifest/browser-capability.generated.js.map +1 -0
  70. package/dist/manifest/discover-base-classes.js +1 -1
  71. package/dist/manifest/discover-smrt-packages.js +1 -1
  72. package/dist/manifest/generator.js +3 -3
  73. package/dist/manifest/index.js +2 -2
  74. package/dist/manifest/local-manifest-reader.d.ts +48 -0
  75. package/dist/manifest/local-manifest-reader.d.ts.map +1 -0
  76. package/dist/manifest/local-manifest-reader.js +81 -0
  77. package/dist/manifest/local-manifest-reader.js.map +1 -0
  78. package/dist/manifest/manager.d.ts +5 -28
  79. package/dist/manifest/manager.d.ts.map +1 -1
  80. package/dist/manifest/manager.js +10 -56
  81. package/dist/manifest/manager.js.map +1 -1
  82. package/dist/manifest/manifest-loader.d.ts +1 -1
  83. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  84. package/dist/manifest/manifest-loader.js +43 -12
  85. package/dist/manifest/manifest-loader.js.map +1 -1
  86. package/dist/manifest/node-host.d.ts +38 -0
  87. package/dist/manifest/node-host.d.ts.map +1 -0
  88. package/dist/manifest/node-host.js +52 -0
  89. package/dist/manifest/node-host.js.map +1 -0
  90. package/dist/manifest/package-manifest-exports.js +1 -1
  91. package/dist/manifest/static-manifest.d.ts.map +1 -1
  92. package/dist/manifest/static-manifest.js +12 -2
  93. package/dist/manifest/static-manifest.js.map +1 -1
  94. package/dist/manifest/store.d.ts +2 -1
  95. package/dist/manifest/store.d.ts.map +1 -1
  96. package/dist/manifest/store.js +7 -4
  97. package/dist/manifest/store.js.map +1 -1
  98. package/dist/manifest.js +1 -1
  99. package/dist/manifest.json +14 -2
  100. package/dist/native-duckdb.d.ts +28 -0
  101. package/dist/native-duckdb.d.ts.map +1 -0
  102. package/dist/native-duckdb.js +51 -0
  103. package/dist/native-duckdb.js.map +1 -0
  104. package/dist/node_modules/.pnpm/@noble_hashes@2.4.0/node_modules/@noble/hashes/_md.js +169 -0
  105. package/dist/node_modules/.pnpm/@noble_hashes@2.4.0/node_modules/@noble/hashes/_md.js.map +1 -0
  106. package/dist/node_modules/.pnpm/@noble_hashes@2.4.0/node_modules/@noble/hashes/_u64.js +13 -0
  107. package/dist/node_modules/.pnpm/@noble_hashes@2.4.0/node_modules/@noble/hashes/_u64.js.map +1 -0
  108. package/dist/node_modules/.pnpm/@noble_hashes@2.4.0/node_modules/@noble/hashes/sha2.js +198 -0
  109. package/dist/node_modules/.pnpm/@noble_hashes@2.4.0/node_modules/@noble/hashes/sha2.js.map +1 -0
  110. package/dist/node_modules/.pnpm/@noble_hashes@2.4.0/node_modules/@noble/hashes/utils.js +243 -0
  111. package/dist/node_modules/.pnpm/@noble_hashes@2.4.0/node_modules/@noble/hashes/utils.js.map +1 -0
  112. package/dist/object.d.ts +56 -0
  113. package/dist/object.d.ts.map +1 -1
  114. package/dist/object.js +139 -16
  115. package/dist/object.js.map +1 -1
  116. package/dist/postgres-permissions.d.ts.map +1 -1
  117. package/dist/postgres-permissions.js +3 -3
  118. package/dist/postgres-permissions.js.map +1 -1
  119. package/dist/prebuild/index.js +1 -1
  120. package/dist/recipe-demo.d.ts +70 -0
  121. package/dist/recipe-demo.d.ts.map +1 -0
  122. package/dist/recipe-demo.js +130 -0
  123. package/dist/recipe-demo.js.map +1 -0
  124. package/dist/recipe.d.ts +64 -1
  125. package/dist/recipe.d.ts.map +1 -1
  126. package/dist/recipe.js +30 -0
  127. package/dist/recipe.js.map +1 -1
  128. package/dist/registry/class-registration.js +2 -2
  129. package/dist/registry/class-registration.js.map +1 -1
  130. package/dist/registry/db-cache-key.js +4 -5
  131. package/dist/registry/db-cache-key.js.map +1 -1
  132. package/dist/registry/inheritance-resolver.js +1 -1
  133. package/dist/registry/name-resolver.js +1 -1
  134. package/dist/registry/qualified-name-aliases.js +1 -1
  135. package/dist/registry/schema-builder.d.ts.map +1 -1
  136. package/dist/registry/schema-builder.js +3 -3
  137. package/dist/registry/schema-builder.js.map +1 -1
  138. package/dist/registry/types.d.ts +5 -0
  139. package/dist/registry/types.d.ts.map +1 -1
  140. package/dist/registry.d.ts.map +1 -1
  141. package/dist/registry.js +5 -8
  142. package/dist/registry.js.map +1 -1
  143. package/dist/revision-guard.js +1 -1
  144. package/dist/revision-guard.js.map +1 -1
  145. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  146. package/dist/scanner/manifest-generator.js +19 -10
  147. package/dist/scanner/manifest-generator.js.map +1 -1
  148. package/dist/scanner/recipe-demo-pass.d.ts +14 -0
  149. package/dist/scanner/recipe-demo-pass.d.ts.map +1 -0
  150. package/dist/scanner/recipe-demo-pass.js +33 -0
  151. package/dist/scanner/recipe-demo-pass.js.map +1 -0
  152. package/dist/scanner/types.d.ts +9 -1
  153. package/dist/scanner/types.d.ts.map +1 -1
  154. package/dist/scanner/types.js.map +1 -1
  155. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  156. package/dist/schema/ddl/duckdb-strategy.js +1 -1
  157. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  158. package/dist/schema/foreign-key-planner.d.ts.map +1 -1
  159. package/dist/schema/foreign-key-planner.js +1 -1
  160. package/dist/schema/foreign-key-planner.js.map +1 -1
  161. package/dist/schema/generator.d.ts.map +1 -1
  162. package/dist/schema/generator.js +10 -14
  163. package/dist/schema/generator.js.map +1 -1
  164. package/dist/schema/schema-aggregator.js +1 -1
  165. package/dist/scripts/browser-entry-evaluation.mjs +157 -0
  166. package/dist/smrt-knowledge.json +32 -12
  167. package/dist/system/browser.d.ts +16 -0
  168. package/dist/system/browser.d.ts.map +1 -0
  169. package/dist/system/browser.js +6 -0
  170. package/dist/system/index.d.ts +1 -6
  171. package/dist/system/index.d.ts.map +1 -1
  172. package/dist/system/index.js +2 -1
  173. package/dist/testing/database.js +1 -1
  174. package/dist/tools/index.js +1 -1
  175. package/dist/unchanged-indexed.d.ts +85 -0
  176. package/dist/unchanged-indexed.d.ts.map +1 -0
  177. package/dist/unchanged-indexed.js +219 -0
  178. package/dist/unchanged-indexed.js.map +1 -0
  179. package/dist/utils/async-context.d.ts +22 -0
  180. package/dist/utils/async-context.d.ts.map +1 -0
  181. package/dist/utils/async-context.js +27 -0
  182. package/dist/utils/async-context.js.map +1 -0
  183. package/dist/utils/import-project-vite.js +1 -1
  184. package/dist/utils/import-workspace-module.js +1 -1
  185. package/dist/utils/json.browser.d.ts +18 -0
  186. package/dist/utils/json.browser.d.ts.map +1 -0
  187. package/dist/utils/json.browser.js +47 -0
  188. package/dist/utils/json.browser.js.map +1 -0
  189. package/dist/utils/sha256.d.ts +15 -0
  190. package/dist/utils/sha256.d.ts.map +1 -0
  191. package/dist/utils/sha256.js +29 -0
  192. package/dist/utils/sha256.js.map +1 -0
  193. package/dist/vite-plugin/changes-route.js +1 -1
  194. package/dist/vite-plugin/dev-plane-route.js +1 -1
  195. package/dist/vite-plugin/events-route.js +1 -1
  196. package/dist/vite-plugin/import-build-aware.js +1 -1
  197. package/dist/vite-plugin/index.js +2 -2
  198. package/dist/vite-plugin/resources-route.js +1 -1
  199. package/dist/vite-plugin/sveltekit-generator.js +2 -2
  200. package/dist/vite-plugin/sveltekit-path.js +1 -1
  201. package/dist/vite-plugin/sveltekit-register-injection.js +1 -1
  202. package/dist/vite-plugin/sveltekit-route-coordinator.js +1 -1
  203. package/dist/vite-plugin/sync-apply-route.js +1 -1
  204. package/dist/vite-plugin/web-collections.js +2 -2
  205. package/package.json +21 -11
@@ -1 +1 @@
1
- {"version":3,"file":"bus.js","names":[],"sources":["../../src/dispatch/bus.ts"],"sourcesContent":["/**\n * DispatchBus - Central hub for inter-agent communication\n *\n * The DispatchBus provides both in-memory handlers and persistent subscriptions\n * for asynchronous agent-to-agent messaging.\n *\n * @example\n * ```typescript\n * const bus = await createDispatchBus({ db: { type: 'sqlite', url: 'app.db' } });\n *\n * // In-memory handler (immediate)\n * bus.on('campaign.completed', async (payload, metadata) => {\n * console.log(`Campaign ${payload.campaignId} completed`);\n * });\n *\n * // Persistent subscription (processed later)\n * await bus.subscribe({ signalType: 'campaign.*', subscriber: 'fiscus' });\n *\n * // Emit a dispatch\n * await bus.emit('campaign.completed', { campaignId: '123' }, { source: 'suasor' });\n *\n * // Process pending dispatches\n * await bus.process('fiscus');\n * ```\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport { applyPostgresRuntimeTimeouts } from '../postgres-timeouts.js';\nimport { runSerializedAgainstSystemTableBootstrap } from '../system/bootstrap.js';\nimport {\n assertPostgresSystemTimestampsCurrent,\n ensureDispatchSubscriptionsSystemTableCompatibility,\n ensureDispatchSystemTableCompatibility,\n getDatabaseEngine,\n} from '../system/compatibility.js';\nimport {\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { DispatchCollection } from './collections/Dispatches.js';\nimport { DispatchSubscriptionCollection } from './collections/DispatchSubscriptions.js';\nimport { Dispatch } from './models/Dispatch.js';\nimport { DispatchSubscription } from './models/DispatchSubscription.js';\nimport {\n resolveDispatchTenantId,\n resolveDispatchTenantScope,\n} from './tenant-resolver.js';\nimport type {\n DispatchBusOptions,\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchEmitOptions,\n DispatchHandler,\n DispatchListOptions,\n DispatchMetadata,\n DispatchProcessOptions,\n DispatchRetryOptions,\n DispatchSubscribeOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Registered in-memory handler\n */\ninterface RegisteredHandler {\n pattern: string;\n handler: DispatchHandler;\n subscription: DispatchSubscription;\n}\n\n/**\n * Central hub for inter-agent messaging with both in-memory and persistent delivery.\n *\n * The `DispatchBus` combines two complementary delivery models:\n *\n * - **In-memory handlers** (`on(pattern, handler)`) — called synchronously (fire-and-forget)\n * when a dispatch is emitted. Fast, but lost on process restart.\n * - **Persistent subscriptions** (`subscribe({ signalType, subscriber })`) — stored in\n * `_smrt_dispatch_subscriptions` and processed later by calling `process(subscriber, handler)`.\n * Survive restarts; suitable for background workers and scheduled agents.\n *\n * Signal types support single-segment wildcards: `'campaign.*'` matches `'campaign.completed'`\n * and `'campaign.failed'`, but not `'campaign.phase.two'`.\n *\n * Dispatch lifecycle: `pending → processing → completed` (or `failed` on handler error).\n * Use `retry()` to reset failed dispatches back to `pending`.\n *\n * Create instances via `createDispatchBus()` — do not use `new DispatchBus()` directly\n * in application code.\n *\n * @example\n * ```typescript\n * const bus = await createDispatchBus({ db: myDb });\n *\n * // In-memory: immediate, fire-and-forget\n * bus.on('invoice.paid', async (payload) => {\n * console.log('Invoice paid:', payload.invoiceId);\n * });\n *\n * // Persistent: processed by 'notifications' agent on next run\n * await bus.subscribe({ signalType: 'invoice.*', subscriber: 'notifications' });\n *\n * // Emit (notifies in-memory handlers immediately, stores persistent dispatch)\n * await bus.emit('invoice.paid', { invoiceId: 'inv-001' }, { source: 'billing' });\n *\n * // Later, in the notifications agent:\n * await bus.process('notifications', async (payload) => {\n * await sendEmail(payload.invoiceId);\n * });\n * ```\n */\nexport class DispatchBus {\n private db: DatabaseInterface;\n private handlers: Map<string, RegisteredHandler[]> = new Map();\n private initialized: boolean = false;\n\n /**\n * Reserved subscriber name used internally for in-memory `on()` handlers.\n * Callers may not register persistent subscriptions under this name, nor\n * assert it as an emit `source`, so they cannot impersonate the in-memory\n * pseudo-subscriber (S5 #1398).\n */\n private static readonly RESERVED_SUBSCRIBER = '_in_memory_';\n\n /** Maximum stored length of a caller-asserted dispatch source label. */\n private static readonly MAX_SOURCE_LENGTH = 256;\n\n /**\n * Normalize a caller-asserted `source` into untrusted metadata.\n *\n * `source` is a declared label, not an authenticated identity, so it must not\n * be trusted for authorization. This caps its length and **rejects** the\n * reserved internal sentinel (`_in_memory_`) by throwing — the contract is\n * that the sentinel is not an accepted source, so a caller cannot quietly\n * impersonate the in-memory subscriber pseudo-source (S5 #1398). Empty/missing\n * values default to `'unknown'` (unchanged behavior).\n *\n * @throws Error if `source` is the reserved in-memory sentinel.\n */\n private static sanitizeSource(source: string | undefined): string {\n const raw = (source ?? '').trim();\n if (!raw) {\n return 'unknown';\n }\n if (raw === DispatchBus.RESERVED_SUBSCRIBER) {\n throw new Error(\n `DispatchBus.emit: \"${DispatchBus.RESERVED_SUBSCRIBER}\" is a reserved source and cannot be asserted`,\n );\n }\n return raw.slice(0, DispatchBus.MAX_SOURCE_LENGTH);\n }\n\n /**\n * Create a new DispatchBus (use createDispatchBus factory instead)\n */\n constructor(db: DatabaseInterface) {\n this.db = db;\n }\n\n /**\n * Initialize the dispatch tables\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n // `_smrt_dispatch`/`_smrt_dispatch_subscriptions` are framework-owned\n // system tables (part of `ALL_SYSTEM_TABLES`), but this standalone\n // initializer provisions/verifies them independently of a full\n // `ensureSystemTables()` bootstrap. Without serialization, concurrent\n // `DispatchBus` instances against the same already-warmed PostgreSQL\n // database (e.g. multiple `Suasor`/`Agent` instantiations, or concurrent\n // test files sharing one database) race on unguarded DDL — proven to\n // throw `duplicate key value violates unique constraint\n // \"pg_type_typname_nsp_index\"` on concurrent `CREATE TABLE IF NOT\n // EXISTS` (#2861). Fresh DDL always creates timezone-aware columns (see\n // `getSystemTableDDLForEngine()`), so this race does not itself produce a\n // legacy-typed column; a stale `assertPostgresSystemTimestampsCurrent()`\n // guard requires a *separate* concurrent actor to have put some\n // `_smrt_*` table into a legacy-typed state (e.g. a test deliberately\n // simulating a pre-migration database), which this lock does not by\n // itself rule out — see the issue for that residual risk. Running this\n // under the same advisory lock `ensureSystemTables()` uses makes every\n // writer of these tables mutually exclusive instead of merely\n // idempotent, which is what the proven race requires.\n //\n // Resolve the engine from `this.db` — the caller-supplied handle — once,\n // up front. `runSerializedAgainstSystemTableBootstrap()` hands the\n // callback a *different* handle on PostgreSQL (the lock-holding\n // transaction it opens), and that handle is not guaranteed to carry the\n // `url`/`type` (or `config.url`/`config.type`) fields engine detection\n // reads (see the `db.config?.url`-style adapters `getDatabaseEngine()`\n // already supports elsewhere in this package). Recomputing the engine\n // on the transaction handle can therefore silently disagree with the\n // outer gate that just decided `postgres` — `detectEngine('', undefined)`\n // falls back to `sqlite`, which would create the dispatch tables with\n // `TIMESTAMP` instead of `TIMESTAMPTZ` and skip the PostgreSQL timestamp\n // guard entirely. Threading the one resolved `engine` through — as\n // `ensureSystemTables()`/`bootstrapSystemTables()` already thread\n // `typeHint` — keeps the inner detection bound to the outer decision.\n const engine = getDatabaseEngine(this.db);\n await runSerializedAgainstSystemTableBootstrap(\n this.db,\n engine,\n async (tx) => {\n // Create dispatch tables if they don't exist\n const dispatchExists = await DispatchCollection.tableExists(tx, engine);\n if (!dispatchExists) {\n // Split the DDL into separate statements and execute each\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_DISPATCH_TABLE,\n engine,\n )\n .split(';')\n .filter((s) => s.trim());\n for (const stmt of statements) {\n await tx.query(stmt);\n }\n }\n await assertPostgresSystemTimestampsCurrent(tx, engine);\n await ensureDispatchSystemTableCompatibility(tx, engine);\n\n const subsExists = await DispatchSubscriptionCollection.tableExists(\n tx,\n engine,\n );\n if (!subsExists) {\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n engine,\n )\n .split(';')\n .filter((s) => s.trim());\n for (const stmt of statements) {\n await tx.query(stmt);\n }\n }\n await assertPostgresSystemTimestampsCurrent(tx, engine);\n await ensureDispatchSubscriptionsSystemTableCompatibility(tx, engine);\n },\n );\n\n this.initialized = true;\n }\n\n /**\n * Emits a dispatch message.\n *\n * Two things happen atomically (from the caller's perspective):\n * 1. One or more `Dispatch` rows are inserted with `status: 'pending'` into `_smrt_dispatch`.\n * 2. All matching in-memory handlers (registered via `on()`) are invoked\n * fire-and-forget (errors are caught and logged, not thrown back to the caller).\n *\n * Persistent subscriptions registered via `subscribe()` will see this dispatch\n * the next time `process(subscriber, handler)` is called for their subscriber name.\n *\n * **Delivery modes** affect how dispatches are created:\n * - `compete` subscribers share a single dispatch (`target_subscriber = NULL`).\n * First subscriber to `process()` claims it (at-most-once delivery).\n * - `fanout` subscribers each get their own dispatch copy (`target_subscriber` set\n * to the subscriber name), so each processes independently.\n *\n * If no subscriptions exist, one dispatch is created for future processing.\n *\n * **Security (S5 #1398):**\n * - `tenant_id` is derived **server-side** from the active tenant context and\n * is never read from caller options, so it cannot be spoofed. Subscribers in\n * a different tenant cannot see or claim this dispatch (see {@link process}).\n * When there is no active tenant context (system/global), `tenant_id` is\n * `NULL` and the dispatch is visible to all scopes — preserving pre-tenancy\n * behavior.\n * - `options.source` is **caller-asserted, untrusted metadata** — it is a\n * declared label only and must not be relied on as an authenticated emitter\n * identity. It is length-capped and the reserved internal sentinel\n * (`_in_memory_`) is rejected so a caller cannot impersonate the in-memory\n * subscriber pseudo-source.\n *\n * @param type - Signal type string, e.g. `'campaign.completed'` or `'invoice.paid'`\n * @param payload - Any JSON-serializable data to attach to the dispatch\n * @param options.source - Declared (untrusted) name of the emitting agent/component (default `'unknown'`)\n * @param options.sourceId - Optional ID of the specific emitting entity\n * @param options.metadata - Optional additional JSON metadata for the dispatch record\n * @returns A persisted `Dispatch` instance (the compete dispatch, or the first fanout copy if fanout-only)\n *\n * @example\n * ```typescript\n * const dispatch = await bus.emit(\n * 'campaign.completed',\n * { campaignId: 'cmp-001', impressions: 10_000 },\n * { source: 'suasor', sourceId: agentId },\n * );\n * console.log(dispatch.id); // UUID of the created dispatch record\n * ```\n *\n * @see {@link on} for in-memory handlers\n * @see {@link subscribe} for persistent subscriptions\n * @see {@link process} to consume pending dispatches\n */\n async emit(\n type: string,\n payload: unknown,\n options: DispatchEmitOptions = {},\n ): Promise<Dispatch> {\n await this.initialize();\n\n // Derive the tenant scope server-side from the active context. This is the\n // trust anchor for cross-tenant isolation (S5 #1398) — it is never taken\n // from caller options, so it cannot be spoofed. `undefined`/`null` means\n // \"no tenant context\" → tenant_id stays NULL (global dispatch).\n const tenantId = resolveDispatchTenantId() ?? null;\n\n // Treat the caller-asserted source as untrusted metadata.\n const source = DispatchBus.sanitizeSource(options.source);\n\n // Query subscriptions to determine if fan-out copies are needed.\n // This runs on every emit() — uses SQL-level exact matching with\n // in-memory fallback only for wildcard subscriptions to minimize I/O.\n // Scoped to the active tenant so emit only fans out to this tenant's\n // (and, for global emits, global) subscriptions (S5 #1398).\n const matchingSubs = await DispatchSubscriptionCollection.findBySignalType(\n this.db,\n type,\n resolveDispatchTenantScope(),\n );\n\n // Separate into compete and fanout subscribers\n const fanoutSubs = matchingSubs.filter((s) => s.delivery === 'fanout');\n const competeSubs = matchingSubs.filter((s) => s.delivery !== 'fanout');\n\n // Create the base dispatch record\n const dispatch = new Dispatch({\n type,\n source,\n source_id: options.sourceId || null,\n payload: JSON.stringify(payload || {}),\n metadata: JSON.stringify(options.metadata || {}),\n status: 'pending',\n correlation_id: options.correlationId || null,\n tenant_id: tenantId,\n });\n\n // Track first persisted dispatch to return to caller\n let returnDispatch = dispatch;\n\n // For fanout subscribers: create a per-subscriber dispatch copy\n for (const sub of fanoutSubs) {\n const fanoutDispatch = new Dispatch({\n type,\n source,\n source_id: options.sourceId || null,\n payload: JSON.stringify(payload || {}),\n metadata: JSON.stringify(options.metadata || {}),\n status: 'pending',\n target_subscriber: sub.subscriber,\n correlation_id: options.correlationId || null,\n tenant_id: tenantId,\n });\n await DispatchCollection.insert(this.db, fanoutDispatch);\n // If no compete dispatch will be persisted, return the first fanout copy\n if (competeSubs.length === 0 && matchingSubs.length > 0) {\n returnDispatch = fanoutDispatch;\n }\n }\n\n // For compete subscribers (or if no subscriptions found): insert the original\n // dispatch with target_subscriber = NULL so any compete subscriber can claim it\n if (competeSubs.length > 0 || matchingSubs.length === 0) {\n await DispatchCollection.insert(this.db, dispatch);\n }\n\n // Notify in-memory handlers immediately (fire-and-forget)\n this.notifyHandlers(type, payload, dispatch.getMetadata());\n\n return returnDispatch;\n }\n\n /**\n * Registers an in-memory handler for a signal type pattern.\n *\n * In-memory handlers are called immediately (fire-and-forget) during `emit()`.\n * They are stored only in memory and lost on process restart — use `subscribe()`\n * for durable, restart-safe subscriptions.\n *\n * Pattern matching supports a single-segment wildcard `*`:\n * - `'campaign.*'` matches `'campaign.completed'` and `'campaign.failed'`\n * - `'campaign.*'` does **not** match `'campaign.phase.two'`\n *\n * Multiple handlers may be registered for the same pattern — all will be called.\n *\n * @param pattern - Signal type pattern, optionally with a trailing `.*` wildcard\n * @param handler - Async or sync function to call with `(payload, metadata)`\n *\n * @example\n * ```typescript\n * bus.on('invoice.*', async (payload, metadata) => {\n * console.log(`Invoice event from ${metadata.source}:`, payload);\n * });\n * ```\n *\n * @see {@link off} to remove a handler\n * @see {@link subscribe} for persistent (restart-safe) subscriptions\n */\n on(pattern: string, handler: DispatchHandler): void {\n const subscription = new DispatchSubscription({\n signal_type: pattern,\n subscriber: '_in_memory_',\n handler: 'callback',\n });\n\n const registered: RegisteredHandler = { pattern, handler, subscription };\n\n if (!this.handlers.has(pattern)) {\n this.handlers.set(pattern, []);\n }\n this.handlers.get(pattern)?.push(registered);\n }\n\n /**\n * Remove an in-memory handler\n *\n * @param pattern - Signal type pattern\n * @param handler - Handler function to remove\n * @returns True if handler was found and removed\n */\n off(pattern: string, handler: DispatchHandler): boolean {\n const handlers = this.handlers.get(pattern);\n if (!handlers) return false;\n\n const index = handlers.findIndex((h) => h.handler === handler);\n if (index === -1) return false;\n\n handlers.splice(index, 1);\n if (handlers.length === 0) {\n this.handlers.delete(pattern);\n }\n return true;\n }\n\n /**\n * Creates or updates a persistent subscription in the database.\n *\n * Persistent subscriptions survive process restarts. When `process(subscriber, handler)`\n * is called later, all pending dispatches matching this subscriber's signal patterns\n * will be delivered.\n *\n * Calling `subscribe()` with the same `signalType`/`subscriber` pair is idempotent\n * (upsert) — it is safe to call on every agent startup.\n *\n * Set `delivery: 'fanout'` to give each subscriber their own dispatch copy.\n * Default is `'compete'` (at-most-once, first subscriber to process claims it).\n *\n * @param options.signalType - Signal type pattern to subscribe to (wildcards supported)\n * @param options.subscriber - Name that identifies this subscriber (e.g. agent class name)\n * @param options.handler - Optional method name on the subscriber to call (default `'handleDispatch'`)\n * @param options.delivery - `'compete'` (default) for at-most-once, `'fanout'` for per-subscriber copies\n * @param options.enabled - If `false`, subscription is created but disabled (default `true`)\n *\n * @example\n * ```typescript\n * // Subscribe on agent startup (idempotent)\n * await bus.subscribe({ signalType: 'campaign.*', subscriber: 'FiscusAgent' });\n *\n * // Fan-out: each subscriber gets their own dispatch copy\n * await bus.subscribe({\n * signalType: 'campaign.*',\n * subscriber: 'AuditorAgent',\n * delivery: 'fanout',\n * });\n *\n * // Later, process matching pending dispatches\n * await bus.process('FiscusAgent', async (payload, metadata) => { ... });\n * ```\n *\n * @see {@link process} to consume pending dispatches for this subscriber\n * @see {@link unsubscribe} to remove the subscription\n * @see {@link on} for non-persistent in-memory handling\n */\n async subscribe(options: DispatchSubscribeOptions): Promise<void> {\n await this.initialize();\n\n // Gate the subscriber namespace (S5 #1398): require a concrete subscriber\n // name and reject the reserved internal sentinel so callers cannot\n // register a persistent subscription that masquerades as the in-memory\n // pseudo-subscriber.\n const subscriber = (options.subscriber ?? '').trim();\n if (!subscriber) {\n throw new Error('DispatchBus.subscribe requires a non-empty subscriber');\n }\n if (subscriber === DispatchBus.RESERVED_SUBSCRIBER) {\n throw new Error(\n `DispatchBus.subscribe: \"${DispatchBus.RESERVED_SUBSCRIBER}\" is a reserved subscriber name`,\n );\n }\n const signalType = (options.signalType ?? '').trim();\n if (!signalType) {\n throw new Error('DispatchBus.subscribe requires a non-empty signalType');\n }\n\n const subscription = new DispatchSubscription({\n signal_type: signalType,\n subscriber,\n handler: options.handler || 'handleDispatch',\n delivery: options.delivery || 'compete',\n enabled: options.enabled !== false ? 1 : 0,\n // Server-derived tenant scope (never from caller options). NULL when there\n // is no active tenant context (global subscription).\n tenant_id: resolveDispatchTenantId() ?? null,\n });\n\n await DispatchSubscriptionCollection.upsert(this.db, subscription);\n }\n\n /**\n * Remove a persistent subscription\n *\n * @param signalType - Signal type pattern\n * @param subscriber - Subscriber name\n */\n async unsubscribe(signalType: string, subscriber: string): Promise<void> {\n await this.initialize();\n // Scoped to the active tenant so one tenant cannot remove another's\n // subscription (S5 #1398).\n await DispatchSubscriptionCollection.deleteByKey(\n this.db,\n signalType,\n subscriber,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * Processes pending dispatches for a named subscriber.\n *\n * For each matching `pending` dispatch:\n * 1. Sets status to `processing`\n * 2. Calls `handler(payload, metadata)`\n * 3. On success: sets status to `completed`\n * 4. On error: sets status to `failed` (stores the error message)\n *\n * Uses a wildcard-aware query strategy: subscriptions with `*` patterns fetch\n * all pending dispatches and filter in memory; exact-match subscriptions use\n * a direct SQL `IN` query for efficiency.\n *\n * @param subscriber - The subscriber name (must match a `subscribe()` call)\n * @param handler - Function to call for each pending dispatch\n * @param options.limit - Maximum dispatches to process in one call (default 100)\n * @param options.signalTypes - Optional filter to process only specific signal types\n * @returns Number of dispatches successfully processed (excludes failed)\n *\n * @example\n * ```typescript\n * const count = await bus.process('FiscusAgent', async (payload, metadata) => {\n * if (metadata.signalType === 'campaign.completed') {\n * await generateInvoice(payload.campaignId);\n * }\n * });\n * console.log(`Processed ${count} dispatches`);\n * ```\n *\n * @see {@link subscribe} to register the persistent subscription first\n * @see {@link retry} to reset failed dispatches back to pending\n */\n async process(\n subscriber: string,\n handler: DispatchHandler,\n options: DispatchProcessOptions = {},\n ): Promise<number> {\n await this.initialize();\n\n // Resolve the active tenant scope server-side (S5 #1398). This distinguishes\n // three states (see resolveDispatchTenantScope):\n // - tenancy off → no filter; system/global + non-tenant deployments behave\n // exactly as before.\n // - active tenant T → only T's dispatches plus global (NULL) dispatches are\n // claimable, so a subscriber in tenant A cannot see/claim tenant B's.\n // - tenancy on but no active tenant → fail-closed to global (NULL) rows\n // only; a missing context never leaks other tenants' dispatches.\n const tenantScope = resolveDispatchTenantScope();\n\n // Get subscriber's subscriptions, scoped to the active tenant so a tenant-A\n // processor only matches tenant-A (and, when global, global) subscriptions.\n const subscriptions = await DispatchSubscriptionCollection.findBySubscriber(\n this.db,\n subscriber,\n true,\n tenantScope,\n );\n\n if (subscriptions.length === 0) {\n return 0;\n }\n\n // Get signal types (including wildcards)\n const signalTypes = subscriptions.map((s) => s.signalType);\n\n // For non-wildcard subscriptions, we can query directly\n // For wildcards, we need to get all pending and filter\n const hasWildcards = signalTypes.some((t) => t.includes('*'));\n\n // Helper: check if a dispatch should be visible to this subscriber\n const isVisibleToSubscriber = (dispatch: Dispatch): boolean => {\n // Tenant isolation (defense in depth — the SQL queries below also filter).\n if (tenantScope.enforced) {\n if (tenantScope.tenantId === null) {\n // Tenancy on, no active tenant → only global (NULL) rows are visible.\n if (dispatch.tenantId !== null) {\n return false;\n }\n } else if (\n dispatch.tenantId !== null &&\n dispatch.tenantId !== tenantScope.tenantId\n ) {\n // Active tenant → that tenant's rows plus global (NULL) rows only.\n return false;\n }\n }\n if (dispatch.targetSubscriber === subscriber) {\n // Targeted at this subscriber (fanout copy) — always visible\n return true;\n }\n if (dispatch.targetSubscriber !== null) {\n // Targeted at a different subscriber — not visible\n return false;\n }\n // Null target (compete dispatch): only visible if this subscriber\n // has a compete subscription matching this dispatch type\n const matchingSub = subscriptions.find((sub) =>\n sub.matches(dispatch.type),\n );\n return matchingSub?.delivery === 'compete';\n };\n\n let pendingDispatches: Dispatch[];\n if (hasWildcards) {\n // Get all pending dispatches and filter by subscription patterns + target\n const allPending = await DispatchCollection.list(this.db, {\n status: 'pending',\n limit: options.limit || 100,\n tenantScope,\n });\n\n pendingDispatches = allPending.filter(\n (dispatch) =>\n subscriptions.some((sub) => sub.matches(dispatch.type)) &&\n isVisibleToSubscriber(dispatch),\n );\n } else {\n // Direct query for exact signal types (with target_subscriber filter)\n const raw = await DispatchCollection.findPending(\n this.db,\n signalTypes,\n options.limit || 100,\n subscriber,\n tenantScope,\n );\n // Post-filter for delivery mode correctness\n pendingDispatches = raw.filter(isVisibleToSubscriber);\n }\n\n // Filter by specific signal types if provided\n if (options.signalTypes && options.signalTypes.length > 0) {\n pendingDispatches = pendingDispatches.filter((d) =>\n options.signalTypes?.includes(d.type),\n );\n }\n\n let processed = 0;\n\n for (const dispatch of pendingDispatches) {\n // Atomically claim the dispatch (S5 #1398). The conditional UPDATE\n // (WHERE id = ? AND status = 'pending' [AND tenant predicate]) closes the\n // TOCTOU window: if a competing worker — or a system-scoped processor —\n // already moved this row out of 'pending', our claim affects no rows and\n // we skip it, so a compete/global dispatch is processed at most once.\n const claimed = await DispatchCollection.claim(\n this.db,\n dispatch,\n tenantScope,\n );\n if (!claimed) {\n continue;\n }\n\n try {\n // Invoke handler\n await handler(dispatch.payload, dispatch.getMetadata());\n\n // Mark as completed\n dispatch.markCompleted(subscriber);\n await DispatchCollection.update(this.db, dispatch);\n processed++;\n } catch (error) {\n // Mark as failed\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n dispatch.markFailed(errorMessage);\n await DispatchCollection.update(this.db, dispatch);\n }\n }\n\n return processed;\n }\n\n /**\n * Retry failed dispatches\n *\n * Resets failed dispatches to pending status so they can be processed again.\n *\n * @param options - Retry options\n * @returns Number of dispatches reset\n */\n async retry(options: DispatchRetryOptions = {}): Promise<number> {\n await this.initialize();\n\n // Tenant isolation (S5 #1398): only the active tenant's failed dispatches\n // (plus global ones) are eligible — a tenant's retry() must not reset\n // another tenant's rows. Scope is derived server-side; callers cannot widen\n // it.\n const retryable = await DispatchCollection.findRetryable(\n this.db,\n options,\n resolveDispatchTenantScope(),\n );\n\n for (const dispatch of retryable) {\n dispatch.resetForRetry();\n await DispatchCollection.update(this.db, dispatch);\n }\n\n return retryable.length;\n }\n\n /**\n * Clean up old dispatches\n *\n * @param options - Cleanup options\n * @returns Number of dispatches deleted\n */\n async cleanup(\n options: DispatchCleanupOptions = {},\n ): Promise<DispatchCleanupResult> {\n await this.initialize();\n // Tenant isolation (S5 #1398): only the active tenant's dispatches (plus\n // global ones) are deleted — a tenant's cleanup() must not delete another\n // tenant's rows. Scope is derived server-side; callers cannot widen it.\n return DispatchCollection.cleanup(\n this.db,\n options,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * Lists dispatches, scoped to the active tenant context (S5 #1398).\n *\n * The tenant scope is derived **server-side** from the active context and\n * **overrides any caller-supplied scope** — callers cannot select another\n * tenant or widen visibility to all tenants. Scoping rules:\n *\n * - tenancy off → no tenant filter (pre-tenancy behavior).\n * - active tenant T → that tenant's dispatches plus global (NULL) dispatches.\n * - tenancy on but no active tenant → global (NULL) dispatches only\n * (fail-closed; never all tenants).\n *\n * @param options - Non-tenant list filters (status/type/source/etc.). Any\n * `tenantScope` field is ignored and replaced with the server-derived one.\n */\n async list(options: DispatchListOptions = {}): Promise<Dispatch[]> {\n await this.initialize();\n // Strip any caller-supplied scope and inject the server-derived one.\n const { tenantScope: _ignoredCallerScope, ...safeOptions } = options;\n return DispatchCollection.list(this.db, {\n ...safeOptions,\n tenantScope: resolveDispatchTenantScope(),\n });\n }\n\n /**\n * Gets a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same server-derived tenant predicate as {@link list}: a\n * subscriber in tenant A cannot fetch tenant B's dispatch by id, and when\n * tenancy is on with no active tenant only global (NULL) dispatches are\n * returned. Returns `null` when the dispatch exists but is out of scope.\n */\n async get(id: string): Promise<Dispatch | null> {\n await this.initialize();\n return DispatchCollection.getScoped(\n this.db,\n id,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * List all subscriptions\n */\n async listSubscriptions(\n subscriber?: string,\n ): Promise<DispatchSubscription[]> {\n await this.initialize();\n\n // Scoped to the active tenant so listing never leaks another tenant's\n // subscriptions (S5 #1398).\n const tenantScope = resolveDispatchTenantScope();\n\n if (subscriber) {\n return DispatchSubscriptionCollection.findBySubscriber(\n this.db,\n subscriber,\n false,\n tenantScope,\n );\n }\n return DispatchSubscriptionCollection.list(this.db, false, tenantScope);\n }\n\n /**\n * Notify in-memory handlers (fire-and-forget)\n */\n private notifyHandlers(\n type: string,\n payload: unknown,\n metadata: DispatchMetadata,\n ): void {\n // Collect all matching handlers\n const matchingHandlers: RegisteredHandler[] = [];\n\n for (const [, handlers] of this.handlers) {\n for (const registered of handlers) {\n if (registered.subscription.matches(type)) {\n matchingHandlers.push(registered);\n }\n }\n }\n\n // Fire-and-forget - don't await\n for (const registered of matchingHandlers) {\n try {\n const result = registered.handler(payload, metadata);\n // Handle both sync and async handlers\n if (result && typeof result.catch === 'function') {\n void result.catch((error: unknown) => {\n logger.error(\n `DispatchBus: Handler for \"${registered.pattern}\" failed`,\n { error },\n );\n });\n }\n } catch (error) {\n logger.error(\n `DispatchBus: Handler for \"${registered.pattern}\" failed`,\n { error },\n );\n }\n }\n }\n}\n\n/**\n * Create a DispatchBus instance\n *\n * @param options - Bus configuration\n * @returns Initialized DispatchBus\n */\nexport async function createDispatchBus(\n options: DispatchBusOptions = {},\n): Promise<DispatchBus> {\n // Resolve database configuration\n const dbConfig = options.db || options.persistence;\n\n if (!dbConfig) {\n throw new Error('DispatchBus requires a database configuration');\n }\n\n // Get or create database interface\n let db: DatabaseInterface;\n if (typeof dbConfig === 'string') {\n // `DispatchBusOptions.db` does not admit a bare string, but this branch is\n // live code and must not be the one unbounded pool a reader finds while\n // auditing this file (#2377).\n db = await getDatabase(\n applyPostgresRuntimeTimeouts({\n url: dbConfig,\n }) as Parameters<typeof getDatabase>[0],\n );\n } else if ('query' in dbConfig) {\n // Already a DatabaseInterface (has query method)\n db = dbConfig as DatabaseInterface;\n } else if ('type' in dbConfig && 'url' in dbConfig) {\n // Database config object - use getDatabase with type and url.\n // Bounded like every other runtime pool so a dispatch-bus connection is\n // not the one unbounded PostgreSQL client in the process (#2377).\n db = await getDatabase(\n applyPostgresRuntimeTimeouts({\n type: dbConfig.type as 'sqlite' | 'postgres' | 'duckdb',\n url: dbConfig.url,\n }) as Parameters<typeof getDatabase>[0],\n );\n } else {\n throw new Error('Invalid database configuration for DispatchBus');\n }\n\n const bus = new DispatchBus(db);\n await bus.initialize();\n\n return bus;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bus.js","names":[],"sources":["../../src/dispatch/bus.ts"],"sourcesContent":["/**\n * DispatchBus - Central hub for inter-agent communication\n *\n * The DispatchBus provides both in-memory handlers and persistent subscriptions\n * for asynchronous agent-to-agent messaging.\n *\n * @example\n * ```typescript\n * const bus = await createDispatchBus({ db: { type: 'sqlite', url: 'app.db' } });\n *\n * // In-memory handler (immediate)\n * bus.on('campaign.completed', async (payload, metadata) => {\n * console.log(`Campaign ${payload.campaignId} completed`);\n * });\n *\n * // Persistent subscription (processed later)\n * await bus.subscribe({ signalType: 'campaign.*', subscriber: 'fiscus' });\n *\n * // Emit a dispatch\n * await bus.emit('campaign.completed', { campaignId: '123' }, { source: 'suasor' });\n *\n * // Process pending dispatches\n * await bus.process('fiscus');\n * ```\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '../host.js';\nimport { applyPostgresRuntimeTimeouts } from '../postgres-timeouts.js';\nimport { runSerializedAgainstSystemTableBootstrap } from '../system/bootstrap.js';\nimport {\n assertPostgresSystemTimestampsCurrent,\n ensureDispatchSubscriptionsSystemTableCompatibility,\n ensureDispatchSystemTableCompatibility,\n getDatabaseEngine,\n} from '../system/compatibility.js';\nimport {\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { DispatchCollection } from './collections/Dispatches.js';\nimport { DispatchSubscriptionCollection } from './collections/DispatchSubscriptions.js';\nimport { Dispatch } from './models/Dispatch.js';\nimport { DispatchSubscription } from './models/DispatchSubscription.js';\nimport {\n resolveDispatchTenantId,\n resolveDispatchTenantScope,\n} from './tenant-resolver.js';\nimport type {\n DispatchBusOptions,\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchEmitOptions,\n DispatchHandler,\n DispatchListOptions,\n DispatchMetadata,\n DispatchProcessOptions,\n DispatchRetryOptions,\n DispatchSubscribeOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Registered in-memory handler\n */\ninterface RegisteredHandler {\n pattern: string;\n handler: DispatchHandler;\n subscription: DispatchSubscription;\n}\n\n/**\n * Central hub for inter-agent messaging with both in-memory and persistent delivery.\n *\n * The `DispatchBus` combines two complementary delivery models:\n *\n * - **In-memory handlers** (`on(pattern, handler)`) — called synchronously (fire-and-forget)\n * when a dispatch is emitted. Fast, but lost on process restart.\n * - **Persistent subscriptions** (`subscribe({ signalType, subscriber })`) — stored in\n * `_smrt_dispatch_subscriptions` and processed later by calling `process(subscriber, handler)`.\n * Survive restarts; suitable for background workers and scheduled agents.\n *\n * Signal types support single-segment wildcards: `'campaign.*'` matches `'campaign.completed'`\n * and `'campaign.failed'`, but not `'campaign.phase.two'`.\n *\n * Dispatch lifecycle: `pending → processing → completed` (or `failed` on handler error).\n * Use `retry()` to reset failed dispatches back to `pending`.\n *\n * Create instances via `createDispatchBus()` — do not use `new DispatchBus()` directly\n * in application code.\n *\n * @example\n * ```typescript\n * const bus = await createDispatchBus({ db: myDb });\n *\n * // In-memory: immediate, fire-and-forget\n * bus.on('invoice.paid', async (payload) => {\n * console.log('Invoice paid:', payload.invoiceId);\n * });\n *\n * // Persistent: processed by 'notifications' agent on next run\n * await bus.subscribe({ signalType: 'invoice.*', subscriber: 'notifications' });\n *\n * // Emit (notifies in-memory handlers immediately, stores persistent dispatch)\n * await bus.emit('invoice.paid', { invoiceId: 'inv-001' }, { source: 'billing' });\n *\n * // Later, in the notifications agent:\n * await bus.process('notifications', async (payload) => {\n * await sendEmail(payload.invoiceId);\n * });\n * ```\n */\nexport class DispatchBus {\n private db: DatabaseInterface;\n private handlers: Map<string, RegisteredHandler[]> = new Map();\n private initialized: boolean = false;\n\n /**\n * Reserved subscriber name used internally for in-memory `on()` handlers.\n * Callers may not register persistent subscriptions under this name, nor\n * assert it as an emit `source`, so they cannot impersonate the in-memory\n * pseudo-subscriber (S5 #1398).\n */\n private static readonly RESERVED_SUBSCRIBER = '_in_memory_';\n\n /** Maximum stored length of a caller-asserted dispatch source label. */\n private static readonly MAX_SOURCE_LENGTH = 256;\n\n /**\n * Normalize a caller-asserted `source` into untrusted metadata.\n *\n * `source` is a declared label, not an authenticated identity, so it must not\n * be trusted for authorization. This caps its length and **rejects** the\n * reserved internal sentinel (`_in_memory_`) by throwing — the contract is\n * that the sentinel is not an accepted source, so a caller cannot quietly\n * impersonate the in-memory subscriber pseudo-source (S5 #1398). Empty/missing\n * values default to `'unknown'` (unchanged behavior).\n *\n * @throws Error if `source` is the reserved in-memory sentinel.\n */\n private static sanitizeSource(source: string | undefined): string {\n const raw = (source ?? '').trim();\n if (!raw) {\n return 'unknown';\n }\n if (raw === DispatchBus.RESERVED_SUBSCRIBER) {\n throw new Error(\n `DispatchBus.emit: \"${DispatchBus.RESERVED_SUBSCRIBER}\" is a reserved source and cannot be asserted`,\n );\n }\n return raw.slice(0, DispatchBus.MAX_SOURCE_LENGTH);\n }\n\n /**\n * Create a new DispatchBus (use createDispatchBus factory instead)\n */\n constructor(db: DatabaseInterface) {\n this.db = db;\n }\n\n /**\n * Initialize the dispatch tables\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n // `_smrt_dispatch`/`_smrt_dispatch_subscriptions` are framework-owned\n // system tables (part of `ALL_SYSTEM_TABLES`), but this standalone\n // initializer provisions/verifies them independently of a full\n // `ensureSystemTables()` bootstrap. Without serialization, concurrent\n // `DispatchBus` instances against the same already-warmed PostgreSQL\n // database (e.g. multiple `Suasor`/`Agent` instantiations, or concurrent\n // test files sharing one database) race on unguarded DDL — proven to\n // throw `duplicate key value violates unique constraint\n // \"pg_type_typname_nsp_index\"` on concurrent `CREATE TABLE IF NOT\n // EXISTS` (#2861). Fresh DDL always creates timezone-aware columns (see\n // `getSystemTableDDLForEngine()`), so this race does not itself produce a\n // legacy-typed column; a stale `assertPostgresSystemTimestampsCurrent()`\n // guard requires a *separate* concurrent actor to have put some\n // `_smrt_*` table into a legacy-typed state (e.g. a test deliberately\n // simulating a pre-migration database), which this lock does not by\n // itself rule out — see the issue for that residual risk. Running this\n // under the same advisory lock `ensureSystemTables()` uses makes every\n // writer of these tables mutually exclusive instead of merely\n // idempotent, which is what the proven race requires.\n //\n // Resolve the engine from `this.db` — the caller-supplied handle — once,\n // up front. `runSerializedAgainstSystemTableBootstrap()` hands the\n // callback a *different* handle on PostgreSQL (the lock-holding\n // transaction it opens), and that handle is not guaranteed to carry the\n // `url`/`type` (or `config.url`/`config.type`) fields engine detection\n // reads (see the `db.config?.url`-style adapters `getDatabaseEngine()`\n // already supports elsewhere in this package). Recomputing the engine\n // on the transaction handle can therefore silently disagree with the\n // outer gate that just decided `postgres` — `detectEngine('', undefined)`\n // falls back to `sqlite`, which would create the dispatch tables with\n // `TIMESTAMP` instead of `TIMESTAMPTZ` and skip the PostgreSQL timestamp\n // guard entirely. Threading the one resolved `engine` through — as\n // `ensureSystemTables()`/`bootstrapSystemTables()` already thread\n // `typeHint` — keeps the inner detection bound to the outer decision.\n const engine = getDatabaseEngine(this.db);\n await runSerializedAgainstSystemTableBootstrap(\n this.db,\n engine,\n async (tx) => {\n // Create dispatch tables if they don't exist\n const dispatchExists = await DispatchCollection.tableExists(tx, engine);\n if (!dispatchExists) {\n // Split the DDL into separate statements and execute each\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_DISPATCH_TABLE,\n engine,\n )\n .split(';')\n .filter((s) => s.trim());\n for (const stmt of statements) {\n await tx.query(stmt);\n }\n }\n await assertPostgresSystemTimestampsCurrent(tx, engine);\n await ensureDispatchSystemTableCompatibility(tx, engine);\n\n const subsExists = await DispatchSubscriptionCollection.tableExists(\n tx,\n engine,\n );\n if (!subsExists) {\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n engine,\n )\n .split(';')\n .filter((s) => s.trim());\n for (const stmt of statements) {\n await tx.query(stmt);\n }\n }\n await assertPostgresSystemTimestampsCurrent(tx, engine);\n await ensureDispatchSubscriptionsSystemTableCompatibility(tx, engine);\n },\n );\n\n this.initialized = true;\n }\n\n /**\n * Emits a dispatch message.\n *\n * Two things happen atomically (from the caller's perspective):\n * 1. One or more `Dispatch` rows are inserted with `status: 'pending'` into `_smrt_dispatch`.\n * 2. All matching in-memory handlers (registered via `on()`) are invoked\n * fire-and-forget (errors are caught and logged, not thrown back to the caller).\n *\n * Persistent subscriptions registered via `subscribe()` will see this dispatch\n * the next time `process(subscriber, handler)` is called for their subscriber name.\n *\n * **Delivery modes** affect how dispatches are created:\n * - `compete` subscribers share a single dispatch (`target_subscriber = NULL`).\n * First subscriber to `process()` claims it (at-most-once delivery).\n * - `fanout` subscribers each get their own dispatch copy (`target_subscriber` set\n * to the subscriber name), so each processes independently.\n *\n * If no subscriptions exist, one dispatch is created for future processing.\n *\n * **Security (S5 #1398):**\n * - `tenant_id` is derived **server-side** from the active tenant context and\n * is never read from caller options, so it cannot be spoofed. Subscribers in\n * a different tenant cannot see or claim this dispatch (see {@link process}).\n * When there is no active tenant context (system/global), `tenant_id` is\n * `NULL` and the dispatch is visible to all scopes — preserving pre-tenancy\n * behavior.\n * - `options.source` is **caller-asserted, untrusted metadata** — it is a\n * declared label only and must not be relied on as an authenticated emitter\n * identity. It is length-capped and the reserved internal sentinel\n * (`_in_memory_`) is rejected so a caller cannot impersonate the in-memory\n * subscriber pseudo-source.\n *\n * @param type - Signal type string, e.g. `'campaign.completed'` or `'invoice.paid'`\n * @param payload - Any JSON-serializable data to attach to the dispatch\n * @param options.source - Declared (untrusted) name of the emitting agent/component (default `'unknown'`)\n * @param options.sourceId - Optional ID of the specific emitting entity\n * @param options.metadata - Optional additional JSON metadata for the dispatch record\n * @returns A persisted `Dispatch` instance (the compete dispatch, or the first fanout copy if fanout-only)\n *\n * @example\n * ```typescript\n * const dispatch = await bus.emit(\n * 'campaign.completed',\n * { campaignId: 'cmp-001', impressions: 10_000 },\n * { source: 'suasor', sourceId: agentId },\n * );\n * console.log(dispatch.id); // UUID of the created dispatch record\n * ```\n *\n * @see {@link on} for in-memory handlers\n * @see {@link subscribe} for persistent subscriptions\n * @see {@link process} to consume pending dispatches\n */\n async emit(\n type: string,\n payload: unknown,\n options: DispatchEmitOptions = {},\n ): Promise<Dispatch> {\n await this.initialize();\n\n // Derive the tenant scope server-side from the active context. This is the\n // trust anchor for cross-tenant isolation (S5 #1398) — it is never taken\n // from caller options, so it cannot be spoofed. `undefined`/`null` means\n // \"no tenant context\" → tenant_id stays NULL (global dispatch).\n const tenantId = resolveDispatchTenantId() ?? null;\n\n // Treat the caller-asserted source as untrusted metadata.\n const source = DispatchBus.sanitizeSource(options.source);\n\n // Query subscriptions to determine if fan-out copies are needed.\n // This runs on every emit() — uses SQL-level exact matching with\n // in-memory fallback only for wildcard subscriptions to minimize I/O.\n // Scoped to the active tenant so emit only fans out to this tenant's\n // (and, for global emits, global) subscriptions (S5 #1398).\n const matchingSubs = await DispatchSubscriptionCollection.findBySignalType(\n this.db,\n type,\n resolveDispatchTenantScope(),\n );\n\n // Separate into compete and fanout subscribers\n const fanoutSubs = matchingSubs.filter((s) => s.delivery === 'fanout');\n const competeSubs = matchingSubs.filter((s) => s.delivery !== 'fanout');\n\n // Create the base dispatch record\n const dispatch = new Dispatch({\n type,\n source,\n source_id: options.sourceId || null,\n payload: JSON.stringify(payload || {}),\n metadata: JSON.stringify(options.metadata || {}),\n status: 'pending',\n correlation_id: options.correlationId || null,\n tenant_id: tenantId,\n });\n\n // Track first persisted dispatch to return to caller\n let returnDispatch = dispatch;\n\n // For fanout subscribers: create a per-subscriber dispatch copy\n for (const sub of fanoutSubs) {\n const fanoutDispatch = new Dispatch({\n type,\n source,\n source_id: options.sourceId || null,\n payload: JSON.stringify(payload || {}),\n metadata: JSON.stringify(options.metadata || {}),\n status: 'pending',\n target_subscriber: sub.subscriber,\n correlation_id: options.correlationId || null,\n tenant_id: tenantId,\n });\n await DispatchCollection.insert(this.db, fanoutDispatch);\n // If no compete dispatch will be persisted, return the first fanout copy\n if (competeSubs.length === 0 && matchingSubs.length > 0) {\n returnDispatch = fanoutDispatch;\n }\n }\n\n // For compete subscribers (or if no subscriptions found): insert the original\n // dispatch with target_subscriber = NULL so any compete subscriber can claim it\n if (competeSubs.length > 0 || matchingSubs.length === 0) {\n await DispatchCollection.insert(this.db, dispatch);\n }\n\n // Notify in-memory handlers immediately (fire-and-forget)\n this.notifyHandlers(type, payload, dispatch.getMetadata());\n\n return returnDispatch;\n }\n\n /**\n * Registers an in-memory handler for a signal type pattern.\n *\n * In-memory handlers are called immediately (fire-and-forget) during `emit()`.\n * They are stored only in memory and lost on process restart — use `subscribe()`\n * for durable, restart-safe subscriptions.\n *\n * Pattern matching supports a single-segment wildcard `*`:\n * - `'campaign.*'` matches `'campaign.completed'` and `'campaign.failed'`\n * - `'campaign.*'` does **not** match `'campaign.phase.two'`\n *\n * Multiple handlers may be registered for the same pattern — all will be called.\n *\n * @param pattern - Signal type pattern, optionally with a trailing `.*` wildcard\n * @param handler - Async or sync function to call with `(payload, metadata)`\n *\n * @example\n * ```typescript\n * bus.on('invoice.*', async (payload, metadata) => {\n * console.log(`Invoice event from ${metadata.source}:`, payload);\n * });\n * ```\n *\n * @see {@link off} to remove a handler\n * @see {@link subscribe} for persistent (restart-safe) subscriptions\n */\n on(pattern: string, handler: DispatchHandler): void {\n const subscription = new DispatchSubscription({\n signal_type: pattern,\n subscriber: '_in_memory_',\n handler: 'callback',\n });\n\n const registered: RegisteredHandler = { pattern, handler, subscription };\n\n if (!this.handlers.has(pattern)) {\n this.handlers.set(pattern, []);\n }\n this.handlers.get(pattern)?.push(registered);\n }\n\n /**\n * Remove an in-memory handler\n *\n * @param pattern - Signal type pattern\n * @param handler - Handler function to remove\n * @returns True if handler was found and removed\n */\n off(pattern: string, handler: DispatchHandler): boolean {\n const handlers = this.handlers.get(pattern);\n if (!handlers) return false;\n\n const index = handlers.findIndex((h) => h.handler === handler);\n if (index === -1) return false;\n\n handlers.splice(index, 1);\n if (handlers.length === 0) {\n this.handlers.delete(pattern);\n }\n return true;\n }\n\n /**\n * Creates or updates a persistent subscription in the database.\n *\n * Persistent subscriptions survive process restarts. When `process(subscriber, handler)`\n * is called later, all pending dispatches matching this subscriber's signal patterns\n * will be delivered.\n *\n * Calling `subscribe()` with the same `signalType`/`subscriber` pair is idempotent\n * (upsert) — it is safe to call on every agent startup.\n *\n * Set `delivery: 'fanout'` to give each subscriber their own dispatch copy.\n * Default is `'compete'` (at-most-once, first subscriber to process claims it).\n *\n * @param options.signalType - Signal type pattern to subscribe to (wildcards supported)\n * @param options.subscriber - Name that identifies this subscriber (e.g. agent class name)\n * @param options.handler - Optional method name on the subscriber to call (default `'handleDispatch'`)\n * @param options.delivery - `'compete'` (default) for at-most-once, `'fanout'` for per-subscriber copies\n * @param options.enabled - If `false`, subscription is created but disabled (default `true`)\n *\n * @example\n * ```typescript\n * // Subscribe on agent startup (idempotent)\n * await bus.subscribe({ signalType: 'campaign.*', subscriber: 'FiscusAgent' });\n *\n * // Fan-out: each subscriber gets their own dispatch copy\n * await bus.subscribe({\n * signalType: 'campaign.*',\n * subscriber: 'AuditorAgent',\n * delivery: 'fanout',\n * });\n *\n * // Later, process matching pending dispatches\n * await bus.process('FiscusAgent', async (payload, metadata) => { ... });\n * ```\n *\n * @see {@link process} to consume pending dispatches for this subscriber\n * @see {@link unsubscribe} to remove the subscription\n * @see {@link on} for non-persistent in-memory handling\n */\n async subscribe(options: DispatchSubscribeOptions): Promise<void> {\n await this.initialize();\n\n // Gate the subscriber namespace (S5 #1398): require a concrete subscriber\n // name and reject the reserved internal sentinel so callers cannot\n // register a persistent subscription that masquerades as the in-memory\n // pseudo-subscriber.\n const subscriber = (options.subscriber ?? '').trim();\n if (!subscriber) {\n throw new Error('DispatchBus.subscribe requires a non-empty subscriber');\n }\n if (subscriber === DispatchBus.RESERVED_SUBSCRIBER) {\n throw new Error(\n `DispatchBus.subscribe: \"${DispatchBus.RESERVED_SUBSCRIBER}\" is a reserved subscriber name`,\n );\n }\n const signalType = (options.signalType ?? '').trim();\n if (!signalType) {\n throw new Error('DispatchBus.subscribe requires a non-empty signalType');\n }\n\n const subscription = new DispatchSubscription({\n signal_type: signalType,\n subscriber,\n handler: options.handler || 'handleDispatch',\n delivery: options.delivery || 'compete',\n enabled: options.enabled !== false ? 1 : 0,\n // Server-derived tenant scope (never from caller options). NULL when there\n // is no active tenant context (global subscription).\n tenant_id: resolveDispatchTenantId() ?? null,\n });\n\n await DispatchSubscriptionCollection.upsert(this.db, subscription);\n }\n\n /**\n * Remove a persistent subscription\n *\n * @param signalType - Signal type pattern\n * @param subscriber - Subscriber name\n */\n async unsubscribe(signalType: string, subscriber: string): Promise<void> {\n await this.initialize();\n // Scoped to the active tenant so one tenant cannot remove another's\n // subscription (S5 #1398).\n await DispatchSubscriptionCollection.deleteByKey(\n this.db,\n signalType,\n subscriber,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * Processes pending dispatches for a named subscriber.\n *\n * For each matching `pending` dispatch:\n * 1. Sets status to `processing`\n * 2. Calls `handler(payload, metadata)`\n * 3. On success: sets status to `completed`\n * 4. On error: sets status to `failed` (stores the error message)\n *\n * Uses a wildcard-aware query strategy: subscriptions with `*` patterns fetch\n * all pending dispatches and filter in memory; exact-match subscriptions use\n * a direct SQL `IN` query for efficiency.\n *\n * @param subscriber - The subscriber name (must match a `subscribe()` call)\n * @param handler - Function to call for each pending dispatch\n * @param options.limit - Maximum dispatches to process in one call (default 100)\n * @param options.signalTypes - Optional filter to process only specific signal types\n * @returns Number of dispatches successfully processed (excludes failed)\n *\n * @example\n * ```typescript\n * const count = await bus.process('FiscusAgent', async (payload, metadata) => {\n * if (metadata.signalType === 'campaign.completed') {\n * await generateInvoice(payload.campaignId);\n * }\n * });\n * console.log(`Processed ${count} dispatches`);\n * ```\n *\n * @see {@link subscribe} to register the persistent subscription first\n * @see {@link retry} to reset failed dispatches back to pending\n */\n async process(\n subscriber: string,\n handler: DispatchHandler,\n options: DispatchProcessOptions = {},\n ): Promise<number> {\n await this.initialize();\n\n // Resolve the active tenant scope server-side (S5 #1398). This distinguishes\n // three states (see resolveDispatchTenantScope):\n // - tenancy off → no filter; system/global + non-tenant deployments behave\n // exactly as before.\n // - active tenant T → only T's dispatches plus global (NULL) dispatches are\n // claimable, so a subscriber in tenant A cannot see/claim tenant B's.\n // - tenancy on but no active tenant → fail-closed to global (NULL) rows\n // only; a missing context never leaks other tenants' dispatches.\n const tenantScope = resolveDispatchTenantScope();\n\n // Get subscriber's subscriptions, scoped to the active tenant so a tenant-A\n // processor only matches tenant-A (and, when global, global) subscriptions.\n const subscriptions = await DispatchSubscriptionCollection.findBySubscriber(\n this.db,\n subscriber,\n true,\n tenantScope,\n );\n\n if (subscriptions.length === 0) {\n return 0;\n }\n\n // Get signal types (including wildcards)\n const signalTypes = subscriptions.map((s) => s.signalType);\n\n // For non-wildcard subscriptions, we can query directly\n // For wildcards, we need to get all pending and filter\n const hasWildcards = signalTypes.some((t) => t.includes('*'));\n\n // Helper: check if a dispatch should be visible to this subscriber\n const isVisibleToSubscriber = (dispatch: Dispatch): boolean => {\n // Tenant isolation (defense in depth — the SQL queries below also filter).\n if (tenantScope.enforced) {\n if (tenantScope.tenantId === null) {\n // Tenancy on, no active tenant → only global (NULL) rows are visible.\n if (dispatch.tenantId !== null) {\n return false;\n }\n } else if (\n dispatch.tenantId !== null &&\n dispatch.tenantId !== tenantScope.tenantId\n ) {\n // Active tenant → that tenant's rows plus global (NULL) rows only.\n return false;\n }\n }\n if (dispatch.targetSubscriber === subscriber) {\n // Targeted at this subscriber (fanout copy) — always visible\n return true;\n }\n if (dispatch.targetSubscriber !== null) {\n // Targeted at a different subscriber — not visible\n return false;\n }\n // Null target (compete dispatch): only visible if this subscriber\n // has a compete subscription matching this dispatch type\n const matchingSub = subscriptions.find((sub) =>\n sub.matches(dispatch.type),\n );\n return matchingSub?.delivery === 'compete';\n };\n\n let pendingDispatches: Dispatch[];\n if (hasWildcards) {\n // Get all pending dispatches and filter by subscription patterns + target\n const allPending = await DispatchCollection.list(this.db, {\n status: 'pending',\n limit: options.limit || 100,\n tenantScope,\n });\n\n pendingDispatches = allPending.filter(\n (dispatch) =>\n subscriptions.some((sub) => sub.matches(dispatch.type)) &&\n isVisibleToSubscriber(dispatch),\n );\n } else {\n // Direct query for exact signal types (with target_subscriber filter)\n const raw = await DispatchCollection.findPending(\n this.db,\n signalTypes,\n options.limit || 100,\n subscriber,\n tenantScope,\n );\n // Post-filter for delivery mode correctness\n pendingDispatches = raw.filter(isVisibleToSubscriber);\n }\n\n // Filter by specific signal types if provided\n if (options.signalTypes && options.signalTypes.length > 0) {\n pendingDispatches = pendingDispatches.filter((d) =>\n options.signalTypes?.includes(d.type),\n );\n }\n\n let processed = 0;\n\n for (const dispatch of pendingDispatches) {\n // Atomically claim the dispatch (S5 #1398). The conditional UPDATE\n // (WHERE id = ? AND status = 'pending' [AND tenant predicate]) closes the\n // TOCTOU window: if a competing worker — or a system-scoped processor —\n // already moved this row out of 'pending', our claim affects no rows and\n // we skip it, so a compete/global dispatch is processed at most once.\n const claimed = await DispatchCollection.claim(\n this.db,\n dispatch,\n tenantScope,\n );\n if (!claimed) {\n continue;\n }\n\n try {\n // Invoke handler\n await handler(dispatch.payload, dispatch.getMetadata());\n\n // Mark as completed\n dispatch.markCompleted(subscriber);\n await DispatchCollection.update(this.db, dispatch);\n processed++;\n } catch (error) {\n // Mark as failed\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n dispatch.markFailed(errorMessage);\n await DispatchCollection.update(this.db, dispatch);\n }\n }\n\n return processed;\n }\n\n /**\n * Retry failed dispatches\n *\n * Resets failed dispatches to pending status so they can be processed again.\n *\n * @param options - Retry options\n * @returns Number of dispatches reset\n */\n async retry(options: DispatchRetryOptions = {}): Promise<number> {\n await this.initialize();\n\n // Tenant isolation (S5 #1398): only the active tenant's failed dispatches\n // (plus global ones) are eligible — a tenant's retry() must not reset\n // another tenant's rows. Scope is derived server-side; callers cannot widen\n // it.\n const retryable = await DispatchCollection.findRetryable(\n this.db,\n options,\n resolveDispatchTenantScope(),\n );\n\n for (const dispatch of retryable) {\n dispatch.resetForRetry();\n await DispatchCollection.update(this.db, dispatch);\n }\n\n return retryable.length;\n }\n\n /**\n * Clean up old dispatches\n *\n * @param options - Cleanup options\n * @returns Number of dispatches deleted\n */\n async cleanup(\n options: DispatchCleanupOptions = {},\n ): Promise<DispatchCleanupResult> {\n await this.initialize();\n // Tenant isolation (S5 #1398): only the active tenant's dispatches (plus\n // global ones) are deleted — a tenant's cleanup() must not delete another\n // tenant's rows. Scope is derived server-side; callers cannot widen it.\n return DispatchCollection.cleanup(\n this.db,\n options,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * Lists dispatches, scoped to the active tenant context (S5 #1398).\n *\n * The tenant scope is derived **server-side** from the active context and\n * **overrides any caller-supplied scope** — callers cannot select another\n * tenant or widen visibility to all tenants. Scoping rules:\n *\n * - tenancy off → no tenant filter (pre-tenancy behavior).\n * - active tenant T → that tenant's dispatches plus global (NULL) dispatches.\n * - tenancy on but no active tenant → global (NULL) dispatches only\n * (fail-closed; never all tenants).\n *\n * @param options - Non-tenant list filters (status/type/source/etc.). Any\n * `tenantScope` field is ignored and replaced with the server-derived one.\n */\n async list(options: DispatchListOptions = {}): Promise<Dispatch[]> {\n await this.initialize();\n // Strip any caller-supplied scope and inject the server-derived one.\n const { tenantScope: _ignoredCallerScope, ...safeOptions } = options;\n return DispatchCollection.list(this.db, {\n ...safeOptions,\n tenantScope: resolveDispatchTenantScope(),\n });\n }\n\n /**\n * Gets a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same server-derived tenant predicate as {@link list}: a\n * subscriber in tenant A cannot fetch tenant B's dispatch by id, and when\n * tenancy is on with no active tenant only global (NULL) dispatches are\n * returned. Returns `null` when the dispatch exists but is out of scope.\n */\n async get(id: string): Promise<Dispatch | null> {\n await this.initialize();\n return DispatchCollection.getScoped(\n this.db,\n id,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * List all subscriptions\n */\n async listSubscriptions(\n subscriber?: string,\n ): Promise<DispatchSubscription[]> {\n await this.initialize();\n\n // Scoped to the active tenant so listing never leaks another tenant's\n // subscriptions (S5 #1398).\n const tenantScope = resolveDispatchTenantScope();\n\n if (subscriber) {\n return DispatchSubscriptionCollection.findBySubscriber(\n this.db,\n subscriber,\n false,\n tenantScope,\n );\n }\n return DispatchSubscriptionCollection.list(this.db, false, tenantScope);\n }\n\n /**\n * Notify in-memory handlers (fire-and-forget)\n */\n private notifyHandlers(\n type: string,\n payload: unknown,\n metadata: DispatchMetadata,\n ): void {\n // Collect all matching handlers\n const matchingHandlers: RegisteredHandler[] = [];\n\n for (const [, handlers] of this.handlers) {\n for (const registered of handlers) {\n if (registered.subscription.matches(type)) {\n matchingHandlers.push(registered);\n }\n }\n }\n\n // Fire-and-forget - don't await\n for (const registered of matchingHandlers) {\n try {\n const result = registered.handler(payload, metadata);\n // Handle both sync and async handlers\n if (result && typeof result.catch === 'function') {\n void result.catch((error: unknown) => {\n logger.error(\n `DispatchBus: Handler for \"${registered.pattern}\" failed`,\n { error },\n );\n });\n }\n } catch (error) {\n logger.error(\n `DispatchBus: Handler for \"${registered.pattern}\" failed`,\n { error },\n );\n }\n }\n }\n}\n\n/**\n * Create a DispatchBus instance\n *\n * @param options - Bus configuration\n * @returns Initialized DispatchBus\n */\nexport async function createDispatchBus(\n options: DispatchBusOptions = {},\n): Promise<DispatchBus> {\n // Resolve database configuration\n const dbConfig = options.db || options.persistence;\n\n if (!dbConfig) {\n throw new Error('DispatchBus requires a database configuration');\n }\n\n // Get or create database interface\n let db: DatabaseInterface;\n if (typeof dbConfig === 'string') {\n // `DispatchBusOptions.db` does not admit a bare string, but this branch is\n // live code and must not be the one unbounded pool a reader finds while\n // auditing this file (#2377).\n db = await getDatabase(\n applyPostgresRuntimeTimeouts({\n url: dbConfig,\n }) as Parameters<typeof getDatabase>[0],\n );\n } else if ('query' in dbConfig) {\n // Already a DatabaseInterface (has query method)\n db = dbConfig as DatabaseInterface;\n } else if ('type' in dbConfig && 'url' in dbConfig) {\n // Database config object - use getDatabase with type and url.\n // Bounded like every other runtime pool so a dispatch-bus connection is\n // not the one unbounded PostgreSQL client in the process (#2377).\n db = await getDatabase(\n applyPostgresRuntimeTimeouts({\n type: dbConfig.type as 'sqlite' | 'postgres' | 'duckdb',\n url: dbConfig.url,\n }) as Parameters<typeof getDatabase>[0],\n );\n } else {\n throw new Error('Invalid database configuration for DispatchBus');\n }\n\n const bus = new DispatchBus(db);\n await bus.initialize();\n\n return bus;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { type DatabaseInterface } from '@happyvertical/sql';
1
+ import type { DatabaseInterface } from '@happyvertical/sql';
2
2
  /** Whether this exact handle belongs to a currently active framework transaction. */
3
3
  export declare function isFrameworkTransactionHandle(db: DatabaseInterface): boolean;
4
4
  interface QueueableDatabase {
@@ -1 +1 @@
1
- {"version":3,"file":"embedded-write-queue.d.ts","sourceRoot":"","sources":["../src/embedded-write-queue.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,iBAAiB,EAEvB,MAAM,oBAAoB,CAAC;AA6C5B,qFAAqF;AACrF,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAE3E;AAED,UAAU,iBAAiB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAG3E;AA0BD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAwBZ;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,EACzD,sBAAsB,UAAQ,GAC7B,OAAO,CAAC,CAAC,CAAC,CAqCZ"}
1
+ {"version":3,"file":"embedded-write-queue.d.ts","sourceRoot":"","sources":["../src/embedded-write-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AA+C5D,qFAAqF;AACrF,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAE3E;AAED,UAAU,iBAAiB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAG3E;AA0BD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CA6BZ;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,EACzD,sBAAsB,UAAQ,GAC7B,OAAO,CAAC,CAAC,CAAC,CAuCZ"}
@@ -1,5 +1,6 @@
1
- import { NestedTransactionError } from "@happyvertical/sql";
2
- import { AsyncLocalStorage } from "node:async_hooks";
1
+ import { NestedTransactionError } from "./host.js";
2
+ import { noteNativeDuckDbHandle } from "./native-duckdb.js";
3
+ import { createAsyncContext } from "./utils/async-context.js";
3
4
  //#region src/embedded-write-queue.ts
4
5
  /**
5
6
  * In-process write serialization for embedded database engines (#2360).
@@ -38,8 +39,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
38
39
  /** Queue tail per database identity (URL string, or the handle itself). */
39
40
  var urlQueues = /* @__PURE__ */ new Map();
40
41
  var handleQueues = /* @__PURE__ */ new WeakMap();
41
- var activeQueueKeys = new AsyncLocalStorage();
42
- var frameworkTransactions = new AsyncLocalStorage();
42
+ var activeQueueKeys = createAsyncContext();
43
+ var frameworkTransactions = createAsyncContext();
43
44
  /** Whether this exact handle belongs to a currently active framework transaction. */
44
45
  function isFrameworkTransactionHandle(db) {
45
46
  return frameworkTransactions.getStore()?.has(db) === true;
@@ -102,7 +103,7 @@ function queueKey(db) {
102
103
  * has settled. When `serialize` is false the operation runs immediately.
103
104
  */
104
105
  async function withEmbeddedWriteQueue(db, serialize, operation) {
105
- if (!serialize) return operation();
106
+ if (!serialize || !activeQueueKeys.propagatesAcrossAwait) return operation();
106
107
  const key = queueKey(db);
107
108
  const active = activeQueueKeys.getStore();
108
109
  if (active?.has(key)) return operation();
@@ -127,6 +128,7 @@ async function withEmbeddedWriteQueue(db, serialize, operation) {
127
128
  async function withEmbeddedWriteTransaction(db, serialize, operation, reuseUnsupportedNested = false) {
128
129
  const transaction = db.transaction?.bind(db);
129
130
  if (!transaction) throw new Error("Database transaction support is required");
131
+ noteNativeDuckDbHandle(db);
130
132
  return withEmbeddedWriteQueue(db, serialize, async () => {
131
133
  let entered = false;
132
134
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DatabaseInterface,\n NestedTransactionError,\n} from '@happyvertical/sql';\n\n/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per embedded database for:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n * - every model save and delete, so the embedded revision-CAS compare/upsert\n * fallback cannot race an ordinary save, natural-key create, or delete.\n * - a complete root `SmrtObject.withTransaction()` callback, with writes on\n * its bound handle re-entering the same hold instead of self-deadlocking.\n *\n * Ordinary save/delete holds contain database calls only — never an\n * interceptor or hook. Explicit transaction callbacks are caller-controlled,\n * so their bound model writes re-enter the existing hold. PostgreSQL is never\n * serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\nconst activeQueueKeys = new AsyncLocalStorage<ReadonlySet<string | object>>();\nconst frameworkTransactions = new AsyncLocalStorage<\n ReadonlySet<DatabaseInterface>\n>();\n\n/** Whether this exact handle belongs to a currently active framework transaction. */\nexport function isFrameworkTransactionHandle(db: DatabaseInterface): boolean {\n return frameworkTransactions.getStore()?.has(db) === true;\n}\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !isPostgresDatabase(db);\n}\n\n/**\n * Whether `db` is a PostgreSQL handle.\n *\n * The revision compare-and-swap predicate is dialect-specific (#2620): only\n * PostgreSQL needs the millisecond-truncating, timezone-tolerant guard, and\n * only PostgreSQL can evaluate `date_trunc`.\n */\nexport function isPostgresDatabase(db: QueueableDatabase): boolean {\n return /^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\n/**\n * Whether revision compare-and-swap needs the process-local read/upsert path.\n *\n * DuckDB/JSON cannot express the timestamp predicate through the generic\n * update API, and local SQLite has only the process's writers in supported\n * deployments. Remote LibSQL/Turso is different: other processes may write\n * the same database, so it must use one conditional UPDATE at the server.\n */\nexport function usesEmbeddedRevisionFallback(db: QueueableDatabase): boolean {\n const url = db?.url ?? '';\n return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\\/\\//iu.test(url);\n}\n\n/**\n * Whether `url` identifies a SQLite/DuckDB in-memory database that is\n * private to its own connection, as opposed to shared state.\n *\n * Bare `:memory:` and `file::memory:` (no query string) each mint a fresh,\n * connection-private database -- `resolveDuckDBUrl(':memory:')` returns it\n * unchanged, with no per-instance suffix -- so two independent handles that\n * both happen to carry this literal string are NOT the same database and\n * must not share a write-queue identity (#2707). A `cache=shared` query\n * parameter (e.g. `file::memory:?cache=shared`) is the opposite case: it is\n * SQLite's own mechanism for making an in-memory database genuinely shared\n * across connections in the process, so a URL carrying it must keep the\n * ordinary string-keyed (shared) behavior.\n */\nfunction isPerConnectionMemoryUrl(url: string): boolean {\n if (url.includes('cache=shared')) return false;\n return url === ':memory:' || url === 'file::memory:';\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n if (typeof db?.url !== 'string' || db.url.length === 0) return db;\n return isPerConnectionMemoryUrl(db.url) ? db : db.url;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n if (!serialize) return operation();\n\n const key = queueKey(db);\n const active = activeQueueKeys.getStore();\n if (active?.has(key)) return operation();\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const run = () =>\n activeQueueKeys.run(new Set([...(active ?? []), key]), operation);\n const next = previous.then(run, run);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n\n/**\n * Serialize a complete embedded root transaction with ordinary model writes.\n * The transaction handle is registered as a reentrant alias so saves and\n * change-feed appends inside the callback do not queue behind their own root\n * transaction.\n */\nexport async function withEmbeddedWriteTransaction<T>(\n db: DatabaseInterface,\n serialize: boolean,\n operation: (transaction: DatabaseInterface) => Promise<T>,\n reuseUnsupportedNested = false,\n): Promise<T> {\n const transaction = db.transaction?.bind(db);\n if (!transaction) {\n throw new Error('Database transaction support is required');\n }\n return withEmbeddedWriteQueue(db, serialize, async () => {\n let entered = false;\n try {\n return await transaction((bound) => {\n entered = true;\n return frameworkTransactions.run(\n new Set([...(frameworkTransactions.getStore() ?? []), bound]),\n () => {\n if (!serialize) return operation(bound);\n const active = activeQueueKeys.getStore();\n return activeQueueKeys.run(\n new Set([...(active ?? []), queueKey(bound)]),\n () => operation(bound),\n );\n },\n );\n });\n } catch (error) {\n // The SDK explicitly refuses no-savepoint nesting before invoking the\n // callback or touching the enclosing transaction. A helper that needs\n // the caller's atomic scope can use that existing handle directly. Never\n // replay work when the callback ran, even if it threw the same error.\n if (\n reuseUnsupportedNested &&\n !entered &&\n error instanceof NestedTransactionError\n ) {\n return operation(db);\n }\n throw error;\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;AAC3D,IAAM,kBAAkB,IAAI,kBAAgD;AAC5E,IAAM,wBAAwB,IAAI,kBAEhC;;AAGF,SAAgB,6BAA6B,IAAgC;CAC3E,OAAO,sBAAsB,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM;AACvD;;;;;;;AAYA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,mBAAmB,EAAE;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,sBAAsB,KAAK,IAAI,OAAO,EAAE;AACjD;;;;;;;;;AAUA,SAAgB,6BAA6B,IAAgC;CAC3E,MAAM,MAAM,IAAI,OAAO;CACvB,OAAO,mBAAmB,EAAE,KAAK,CAAC,4BAA4B,KAAK,GAAG;AACxE;;;;;;;;;;;;;;;AAgBA,SAAS,yBAAyB,KAAsB;CACtD,IAAI,IAAI,SAAS,cAAc,GAAG,OAAO;CACzC,OAAO,QAAQ,cAAc,QAAQ;AACvC;AAEA,SAAS,SAAS,IAAwC;CACxD,IAAI,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,WAAW,GAAG,OAAO;CAC/D,OAAO,yBAAyB,GAAG,GAAG,IAAI,KAAK,GAAG;AACpD;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CACZ,IAAI,CAAC,WAAW,OAAO,UAAU;CAEjC,MAAM,MAAM,SAAS,EAAE;CACvB,MAAM,SAAS,gBAAgB,SAAS;CACxC,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,UAAU;CACvC,MAAM,YACH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ;CAClB,MAAM,YACJ,gBAAgB,oBAAI,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG,CAAC,GAAG,SAAS;CAClE,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;CAGnC,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT;;;;;;;AAQA,eAAsB,6BACpB,IACA,WACA,WACA,yBAAyB,OACb;CACZ,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE;CAC3C,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO,uBAAuB,IAAI,WAAW,YAAY;EACvD,IAAI,UAAU;EACd,IAAI;GACF,OAAO,MAAM,aAAa,UAAU;IAClC,UAAU;IACV,OAAO,sBAAsB,oBAC3B,IAAI,IAAI,CAAC,GAAI,sBAAsB,SAAS,KAAK,CAAC,GAAI,KAAK,CAAC,SACtD;KACJ,IAAI,CAAC,WAAW,OAAO,UAAU,KAAK;KACtC,MAAM,SAAS,gBAAgB,SAAS;KACxC,OAAO,gBAAgB,oBACrB,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,SAAS,KAAK,CAAC,CAAC,SACtC,UAAU,KAAK,CACvB;IACF,CACF;GACF,CAAC;EACH,SAAS,OAAO;GAKd,IACE,0BACA,CAAC,WACD,iBAAiB,wBAEjB,OAAO,UAAU,EAAE;GAErB,MAAM;EACR;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["import type { DatabaseInterface } from '@happyvertical/sql';\nimport { NestedTransactionError } from './host.js';\nimport { noteNativeDuckDbHandle } from './native-duckdb.js';\nimport { createAsyncContext } from './utils/async-context.js';\n\n/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per embedded database for:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n * - every model save and delete, so the embedded revision-CAS compare/upsert\n * fallback cannot race an ordinary save, natural-key create, or delete.\n * - a complete root `SmrtObject.withTransaction()` callback, with writes on\n * its bound handle re-entering the same hold instead of self-deadlocking.\n *\n * Ordinary save/delete holds contain database calls only — never an\n * interceptor or hook. Explicit transaction callbacks are caller-controlled,\n * so their bound model writes re-enter the existing hold. PostgreSQL is never\n * serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\nconst activeQueueKeys = createAsyncContext<ReadonlySet<string | object>>();\nconst frameworkTransactions =\n createAsyncContext<ReadonlySet<DatabaseInterface>>();\n\n/** Whether this exact handle belongs to a currently active framework transaction. */\nexport function isFrameworkTransactionHandle(db: DatabaseInterface): boolean {\n return frameworkTransactions.getStore()?.has(db) === true;\n}\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !isPostgresDatabase(db);\n}\n\n/**\n * Whether `db` is a PostgreSQL handle.\n *\n * The revision compare-and-swap predicate is dialect-specific (#2620): only\n * PostgreSQL needs the millisecond-truncating, timezone-tolerant guard, and\n * only PostgreSQL can evaluate `date_trunc`.\n */\nexport function isPostgresDatabase(db: QueueableDatabase): boolean {\n return /^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\n/**\n * Whether revision compare-and-swap needs the process-local read/upsert path.\n *\n * DuckDB/JSON cannot express the timestamp predicate through the generic\n * update API, and local SQLite has only the process's writers in supported\n * deployments. Remote LibSQL/Turso is different: other processes may write\n * the same database, so it must use one conditional UPDATE at the server.\n */\nexport function usesEmbeddedRevisionFallback(db: QueueableDatabase): boolean {\n const url = db?.url ?? '';\n return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\\/\\//iu.test(url);\n}\n\n/**\n * Whether `url` identifies a SQLite/DuckDB in-memory database that is\n * private to its own connection, as opposed to shared state.\n *\n * Bare `:memory:` and `file::memory:` (no query string) each mint a fresh,\n * connection-private database -- `resolveDuckDBUrl(':memory:')` returns it\n * unchanged, with no per-instance suffix -- so two independent handles that\n * both happen to carry this literal string are NOT the same database and\n * must not share a write-queue identity (#2707). A `cache=shared` query\n * parameter (e.g. `file::memory:?cache=shared`) is the opposite case: it is\n * SQLite's own mechanism for making an in-memory database genuinely shared\n * across connections in the process, so a URL carrying it must keep the\n * ordinary string-keyed (shared) behavior.\n */\nfunction isPerConnectionMemoryUrl(url: string): boolean {\n if (url.includes('cache=shared')) return false;\n return url === ':memory:' || url === 'file::memory:';\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n if (typeof db?.url !== 'string' || db.url.length === 0) return db;\n return isPerConnectionMemoryUrl(db.url) ? db : db.url;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n // Re-entry into a held queue is recognized through async-local state. A host\n // without it (a browser) cannot tell a nested write from an independent one,\n // and queueing the nested write behind its own parent would deadlock, so it\n // runs unqueued there; its only database is PGlite, which serializes\n // statements itself.\n if (!serialize || !activeQueueKeys.propagatesAcrossAwait) return operation();\n\n const key = queueKey(db);\n const active = activeQueueKeys.getStore();\n if (active?.has(key)) return operation();\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const run = () =>\n activeQueueKeys.run(new Set([...(active ?? []), key]), operation);\n const next = previous.then(run, run);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n\n/**\n * Serialize a complete embedded root transaction with ordinary model writes.\n * The transaction handle is registered as a reentrant alias so saves and\n * change-feed appends inside the callback do not queue behind their own root\n * transaction.\n */\nexport async function withEmbeddedWriteTransaction<T>(\n db: DatabaseInterface,\n serialize: boolean,\n operation: (transaction: DatabaseInterface) => Promise<T>,\n reuseUnsupportedNested = false,\n): Promise<T> {\n const transaction = db.transaction?.bind(db);\n if (!transaction) {\n throw new Error('Database transaction support is required');\n }\n // The transaction handle carries no adapter markers: identify the root.\n noteNativeDuckDbHandle(db);\n return withEmbeddedWriteQueue(db, serialize, async () => {\n let entered = false;\n try {\n return await transaction((bound) => {\n entered = true;\n return frameworkTransactions.run(\n new Set([...(frameworkTransactions.getStore() ?? []), bound]),\n () => {\n if (!serialize) return operation(bound);\n const active = activeQueueKeys.getStore();\n return activeQueueKeys.run(\n new Set([...(active ?? []), queueKey(bound)]),\n () => operation(bound),\n );\n },\n );\n });\n } catch (error) {\n // The SDK explicitly refuses no-savepoint nesting before invoking the\n // callback or touching the enclosing transaction. A helper that needs\n // the caller's atomic scope can use that existing handle directly. Never\n // replay work when the callback ran, even if it threw the same error.\n if (\n reuseUnsupportedNested &&\n !entered &&\n error instanceof NestedTransactionError\n ) {\n return operation(db);\n }\n throw error;\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;AAC3D,IAAM,kBAAkB,mBAAiD;AACzE,IAAM,wBACJ,mBAAmD;;AAGrD,SAAgB,6BAA6B,IAAgC;CAC3E,OAAO,sBAAsB,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM;AACvD;;;;;;;AAYA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,mBAAmB,EAAE;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,sBAAsB,KAAK,IAAI,OAAO,EAAE;AACjD;;;;;;;;;AAUA,SAAgB,6BAA6B,IAAgC;CAC3E,MAAM,MAAM,IAAI,OAAO;CACvB,OAAO,mBAAmB,EAAE,KAAK,CAAC,4BAA4B,KAAK,GAAG;AACxE;;;;;;;;;;;;;;;AAgBA,SAAS,yBAAyB,KAAsB;CACtD,IAAI,IAAI,SAAS,cAAc,GAAG,OAAO;CACzC,OAAO,QAAQ,cAAc,QAAQ;AACvC;AAEA,SAAS,SAAS,IAAwC;CACxD,IAAI,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,WAAW,GAAG,OAAO;CAC/D,OAAO,yBAAyB,GAAG,GAAG,IAAI,KAAK,GAAG;AACpD;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CAMZ,IAAI,CAAC,aAAa,CAAC,gBAAgB,uBAAuB,OAAO,UAAU;CAE3E,MAAM,MAAM,SAAS,EAAE;CACvB,MAAM,SAAS,gBAAgB,SAAS;CACxC,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,UAAU;CACvC,MAAM,YACH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ;CAClB,MAAM,YACJ,gBAAgB,oBAAI,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG,CAAC,GAAG,SAAS;CAClE,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;CAGnC,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT;;;;;;;AAQA,eAAsB,6BACpB,IACA,WACA,WACA,yBAAyB,OACb;CACZ,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE;CAC3C,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,0CAA0C;CAG5D,uBAAuB,EAAE;CACzB,OAAO,uBAAuB,IAAI,WAAW,YAAY;EACvD,IAAI,UAAU;EACd,IAAI;GACF,OAAO,MAAM,aAAa,UAAU;IAClC,UAAU;IACV,OAAO,sBAAsB,oBAC3B,IAAI,IAAI,CAAC,GAAI,sBAAsB,SAAS,KAAK,CAAC,GAAI,KAAK,CAAC,SACtD;KACJ,IAAI,CAAC,WAAW,OAAO,UAAU,KAAK;KACtC,MAAM,SAAS,gBAAgB,SAAS;KACxC,OAAO,gBAAgB,oBACrB,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,SAAS,KAAK,CAAC,CAAC,SACtC,UAAU,KAAK,CACvB;IACF,CACF;GACF,CAAC;EACH,SAAS,OAAO;GAKd,IACE,0BACA,CAAC,WACD,iBAAiB,wBAEjB,OAAO,UAAU,EAAE;GAErB,MAAM;EACR;CACF,CAAC;AACH"}
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { sha256Hex } from "../utils/sha256.js";
2
2
  //#region src/embeddings/hash.ts
3
3
  /**
4
4
  * Content Hashing for Embedding Change Detection
@@ -16,7 +16,7 @@ var ContentHasher = class ContentHasher {
16
16
  * @returns Hex-encoded hash string
17
17
  */
18
18
  static hash(content) {
19
- return createHash("sha256").update(content).digest("hex");
19
+ return sha256Hex(content);
20
20
  }
21
21
  /**
22
22
  * Check if content has changed by comparing hashes
@@ -1 +1 @@
1
- {"version":3,"file":"hash.js","names":[],"sources":["../../src/embeddings/hash.ts"],"sourcesContent":["/**\n * Content Hashing for Embedding Change Detection\n *\n * Uses SHA-256 to detect when content has changed and embeddings need regeneration.\n */\n\nimport { createHash } from 'node:crypto';\n\n/**\n * Content hasher for embedding change detection\n */\nexport class ContentHasher {\n /**\n * Generate SHA-256 hash of content\n *\n * @param content - Text content to hash\n * @returns Hex-encoded hash string\n */\n static hash(content: string): string {\n return createHash('sha256').update(content).digest('hex');\n }\n\n /**\n * Check if content has changed by comparing hashes\n *\n * @param content - Current content\n * @param previousHash - Previously stored hash\n * @returns True if content has changed\n */\n static hasChanged(content: string, previousHash: string): boolean {\n return ContentHasher.hash(content) !== previousHash;\n }\n\n /**\n * Generate hash for multiple fields combined\n *\n * @param fields - Object with field names and values\n * @returns Hex-encoded hash of combined content\n */\n static hashFields(fields: Record<string, string>): string {\n // Sort keys for deterministic ordering\n const sortedKeys = Object.keys(fields).sort();\n const combined = sortedKeys\n .map((key) => `${key}:${fields[key]}`)\n .join('\\n');\n return ContentHasher.hash(combined);\n }\n}\n"],"mappings":";;;;;;;;;;AAWA,IAAa,gBAAb,MAAa,cAAc;;;;;;;CAOzB,OAAO,KAAK,SAAyB;EACnC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,OAAO,KAAK;CAC1D;;;;;;;;CASA,OAAO,WAAW,SAAiB,cAA+B;EAChE,OAAO,cAAc,KAAK,OAAO,MAAM;CACzC;;;;;;;CAQA,OAAO,WAAW,QAAwC;EAGxD,MAAM,WADa,OAAO,KAAK,MAAM,CAAC,CAAC,KACtB,CAAA,CACd,KAAK,QAAQ,GAAG,IAAI,GAAG,OAAO,MAAM,CAAC,CACrC,KAAK,IAAI;EACZ,OAAO,cAAc,KAAK,QAAQ;CACpC;AACF"}
1
+ {"version":3,"file":"hash.js","names":[],"sources":["../../src/embeddings/hash.ts"],"sourcesContent":["/**\n * Content Hashing for Embedding Change Detection\n *\n * Uses SHA-256 to detect when content has changed and embeddings need regeneration.\n */\n\nimport { sha256Hex } from '../utils/sha256.js';\n\n/**\n * Content hasher for embedding change detection\n */\nexport class ContentHasher {\n /**\n * Generate SHA-256 hash of content\n *\n * @param content - Text content to hash\n * @returns Hex-encoded hash string\n */\n static hash(content: string): string {\n return sha256Hex(content);\n }\n\n /**\n * Check if content has changed by comparing hashes\n *\n * @param content - Current content\n * @param previousHash - Previously stored hash\n * @returns True if content has changed\n */\n static hasChanged(content: string, previousHash: string): boolean {\n return ContentHasher.hash(content) !== previousHash;\n }\n\n /**\n * Generate hash for multiple fields combined\n *\n * @param fields - Object with field names and values\n * @returns Hex-encoded hash of combined content\n */\n static hashFields(fields: Record<string, string>): string {\n // Sort keys for deterministic ordering\n const sortedKeys = Object.keys(fields).sort();\n const combined = sortedKeys\n .map((key) => `${key}:${fields[key]}`)\n .join('\\n');\n return ContentHasher.hash(combined);\n }\n}\n"],"mappings":";;;;;;;;;;AAWA,IAAa,gBAAb,MAAa,cAAc;;;;;;;CAOzB,OAAO,KAAK,SAAyB;EACnC,OAAO,UAAU,OAAO;CAC1B;;;;;;;;CASA,OAAO,WAAW,SAAiB,cAA+B;EAChE,OAAO,cAAc,KAAK,OAAO,MAAM;CACzC;;;;;;;CAQA,OAAO,WAAW,QAAwC;EAGxD,MAAM,WADa,OAAO,KAAK,MAAM,CAAC,CAAC,KACtB,CAAA,CACd,KAAK,QAAQ,GAAG,IAAI,GAAG,OAAO,MAAM,CAAC,CACrC,KAAK,IAAI;EACZ,OAAO,cAAc,KAAK,QAAQ;CACpC;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/embeddings/storage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/embeddings/storage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAIhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAS,CAAC;AAO/C;;GAEG;AACH,qBAAa,gBAAgB;IAC3B;;;;;;OAMG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,IAAI,EAAE;QACJ,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,EACD,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC;IA6ChB;;;;;;;;;;;;;OAaG;WACU,aAAa,CACxB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB;;;;;WAKG;QACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,EACN,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IA+G3D;;;;;OAKG;WACU,oBAAoB,CAC/B,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;KACvD,GACA,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IA+C3D;;;;;;;OAOG;WACU,mBAAmB,CAC9B,GAAG,EAAE,iBAAiB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;;;;OASG;WACU,GAAG,CACd,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAclC;;;;;;;OAOG;WACU,YAAY,CACvB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,EAAE,CAAC;IAY7B;;;;;;;;OAQG;WACU,YAAY,CACvB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,EAAE,CAAC;IAqB7B;;;;;;;OAOG;WACU,0BAA0B,CACrC,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,EAAE,CAAC;IAWpB;;;;;;OAMG;WACU,eAAe,CAC1B,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAQhB;;;;;;;;OAQG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;OAKG;WACU,WAAW,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IASjE;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ;CAexB"}
@@ -2,7 +2,6 @@ import { chunkArray } from "../utils/chunk.js";
2
2
  import { toSafeInteger } from "../utils/safe-integer.js";
3
3
  import { CosineSimilarity } from "./similarity.js";
4
4
  import { createLogger } from "@happyvertical/logger";
5
- import { randomUUID } from "node:crypto";
6
5
  //#region src/embeddings/storage.ts
7
6
  /**
8
7
  * Embedding Storage Operations
@@ -26,7 +25,7 @@ var EmbeddingStorage = class EmbeddingStorage {
26
25
  */
27
26
  static async upsert(db, data, vector) {
28
27
  const now = (/* @__PURE__ */ new Date()).toISOString();
29
- const id = randomUUID();
28
+ const id = globalThis.crypto.randomUUID();
30
29
  await db.upsert("_smrt_embeddings", [
31
30
  "object_class",
32
31
  "object_id",
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","names":[],"sources":["../../src/embeddings/storage.ts"],"sourcesContent":["/**\n * Embedding Storage Operations\n *\n * CRUD operations for the _smrt_embeddings system table.\n * Supports both JSON-based and native vector storage strategies.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface, VectorCapabilities } from '@happyvertical/sql';\nimport { chunkArray, IN_LIST_CHUNK_SIZE } from '../utils/chunk';\nimport { toSafeInteger } from '../utils/safe-integer.js';\nimport { CosineSimilarity } from './similarity';\nimport type { StoredEmbedding } from './types';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Column name for native vector storage */\nconst VECTOR_COLUMN = 'embedding_vector';\n\n/**\n * Storage operations for embeddings in _smrt_embeddings table\n */\nexport class EmbeddingStorage {\n /**\n * Insert or update an embedding\n *\n * @param db - Database interface\n * @param data - Embedding data to store\n * @param vector - Optional vector capabilities for native storage\n */\n static async upsert(\n db: DatabaseInterface,\n data: {\n objectClass: string;\n objectId: string;\n fieldName: string;\n contentHash: string;\n embedding: number[];\n model: string;\n dimensions: number;\n provider?: string;\n },\n vector?: VectorCapabilities,\n ): Promise<void> {\n const now = new Date().toISOString();\n const id = randomUUID();\n\n await db.upsert(\n '_smrt_embeddings',\n ['object_class', 'object_id', 'field_name', 'model'],\n {\n id,\n object_class: data.objectClass,\n object_id: data.objectId,\n field_name: data.fieldName,\n content_hash: data.contentHash,\n embedding: JSON.stringify(data.embedding),\n model: data.model,\n dimensions: data.dimensions,\n provider: data.provider || null,\n created_at: now,\n updated_at: now,\n },\n );\n\n // Also store native vector if capability is available\n if (vector) {\n try {\n await vector.upsertVector(\n '_smrt_embeddings',\n {\n object_class: data.objectClass,\n object_id: data.objectId,\n field_name: data.fieldName,\n model: data.model,\n },\n VECTOR_COLUMN,\n data.embedding,\n );\n } catch (error) {\n // Log but don't fail — JSON fallback is always available\n logger.warn(\n `[embeddings] Failed to store native vector: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n /**\n * Search for similar embeddings using the best available strategy.\n *\n * When vector capabilities are provided, delegates to database-level\n * vector search. Otherwise falls back to loading all embeddings and\n * computing cosine similarity in memory.\n *\n * @param db - Database interface\n * @param objectClass - Class name to search within\n * @param embedding - Query embedding vector\n * @param options - Search options\n * @param vector - Optional vector capabilities for native search\n * @returns Array of { objectId, similarity } ranked by similarity (highest first)\n */\n static async searchSimilar(\n db: DatabaseInterface,\n objectClass: string,\n embedding: number[],\n options: {\n field?: string;\n model?: string;\n limit?: number;\n minSimilarity?: number;\n /**\n * Restrict candidates to these object ids BEFORE ranking (#2365).\n * `undefined` means no restriction; an empty array means no candidates\n * (the search returns no results). Used by tenant-scoped collections so\n * top-K never ranks rows the caller is not allowed to see.\n */\n objectIds?: string[];\n } = {},\n vector?: VectorCapabilities,\n ): Promise<Array<{ objectId: string; similarity: number }>> {\n const { field, model, limit = 10, minSimilarity = 0, objectIds } = options;\n\n // An explicit empty candidate set means \"nothing is visible\" — never fall\n // through to the unrestricted search.\n if (objectIds !== undefined && objectIds.length === 0) {\n return [];\n }\n\n // Native vector search path\n if (vector) {\n try {\n // Build WHERE clause for class/field/model filtering.\n // Parameter numbering starts at $2 because vector.search() reserves\n // $1 for the query vector embedding in its internal SQL query.\n const conditions: string[] = ['object_class = $2'];\n const params: unknown[] = [objectClass];\n\n if (field) {\n conditions.push(`field_name = $${params.length + 2}`);\n params.push(field);\n }\n\n if (model) {\n conditions.push(`model = $${params.length + 2}`);\n params.push(model);\n }\n\n // Candidate restriction (#2365). The id list is chunked so one query\n // never exceeds the backend's bind-variable limit; per-chunk top-K\n // results merge into an exact global top-K because every chunk\n // returns its own best `limit` candidates.\n const idChunks = objectIds\n ? chunkArray(objectIds, IN_LIST_CHUNK_SIZE)\n : [undefined];\n\n const merged: Array<{ objectId: string; similarity: number }> = [];\n for (const chunk of idChunks) {\n const chunkConditions = [...conditions];\n const chunkParams = [...params];\n if (chunk) {\n const placeholders = chunk.map(\n (_, index) => `$${chunkParams.length + 2 + index}`,\n );\n chunkConditions.push(`object_id IN (${placeholders.join(', ')})`);\n chunkParams.push(...chunk);\n }\n\n const results = await vector.search(\n '_smrt_embeddings',\n VECTOR_COLUMN,\n embedding,\n {\n limit,\n metric: 'cosine',\n where: chunkConditions.join(' AND '),\n params: chunkParams,\n },\n );\n\n // Convert cosine distance (0=identical, 2=opposite) to similarity\n // (1=identical, -1=opposite)\n merged.push(\n ...results.map((r) => ({\n objectId: r.object_id as string,\n similarity: 1 - r.distance,\n })),\n );\n }\n\n return merged\n .filter((r) => r.similarity >= minSimilarity)\n .sort((a, b) => b.similarity - a.similarity)\n .slice(0, limit);\n } catch (error) {\n // Fall back to in-memory search on vector search failure\n logger.warn(\n `[embeddings] Native vector search failed, falling back to in-memory: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n // In-memory fallback: load all embeddings and compute cosine similarity\n const storedEmbeddings = await EmbeddingStorage.listForClass(\n db,\n objectClass,\n field,\n model,\n );\n\n // Candidate restriction (#2365): drop invisible rows BEFORE ranking so\n // top-K is computed only over what the caller may see.\n const allowedIds = objectIds ? new Set(objectIds) : undefined;\n const candidates = allowedIds\n ? storedEmbeddings.filter((stored) => allowedIds.has(stored.object_id))\n : storedEmbeddings;\n\n if (candidates.length === 0) {\n return [];\n }\n\n return candidates\n .map((stored) => ({\n objectId: stored.object_id,\n similarity: CosineSimilarity.calculate(embedding, stored.embedding),\n }))\n .filter((item) => item.similarity >= minSimilarity)\n .sort((a, b) => b.similarity - a.similarity)\n .slice(0, limit);\n }\n\n /**\n * Exact cosine top-K using bounded embedding pages and a caller-owned scope.\n * JSON vectors are always persisted, including with native vector storage.\n * Eligibility is checked before scoring; neither all vectors nor all eligible\n * object IDs are materialized. The callback must fail closed on scope errors.\n */\n static async searchSimilarBatched(\n db: DatabaseInterface,\n objectClass: string,\n embedding: number[],\n options: {\n field: string;\n model: string;\n limit: number;\n minSimilarity: number;\n eligible: (objectIds: string[]) => Promise<boolean[]>;\n },\n ): Promise<Array<{ objectId: string; similarity: number }>> {\n const batchSize = 64;\n let cursor: string | undefined;\n let best: Array<{ objectId: string; similarity: number }> = [];\n for (;;) {\n const params: unknown[] = [objectClass, options.field, options.model];\n const cursorPredicate = cursor === undefined ? '' : ' AND id > ?';\n if (cursor !== undefined) params.push(cursor);\n const { rows } = await db.query(\n `SELECT CAST(id AS VARCHAR) AS id, CAST(object_id AS VARCHAR) AS object_id, embedding FROM _smrt_embeddings\n WHERE object_class = ? AND field_name = ? AND model = ?${cursorPredicate}\n ORDER BY _smrt_embeddings.id ASC LIMIT ${batchSize}`,\n ...params,\n );\n if (rows.length === 0) break;\n const allowed = await options.eligible(\n rows.map((row: Record<string, unknown>) => String(row.object_id)),\n );\n if (\n allowed.length !== rows.length ||\n allowed.some((value) => typeof value !== 'boolean')\n ) {\n throw new Error('Invalid embedding candidate eligibility mask');\n }\n for (let index = 0; index < rows.length; index++) {\n if (!allowed[index]) continue;\n const row = rows[index];\n const similarity = CosineSimilarity.calculate(\n embedding,\n JSON.parse(row.embedding as string) as number[],\n );\n if (similarity >= options.minSimilarity) {\n best.push({ objectId: String(row.object_id), similarity });\n }\n }\n best.sort(\n (a, b) =>\n b.similarity - a.similarity ||\n (a.objectId < b.objectId ? -1 : a.objectId > b.objectId ? 1 : 0),\n );\n best = best.slice(0, options.limit);\n cursor = String(rows[rows.length - 1].id);\n if (rows.length < batchSize) break;\n }\n return best;\n }\n\n /**\n * Ensure the native vector column and index exist on _smrt_embeddings.\n * Called once during initialization when storage: 'native' is configured.\n *\n * @param db - Database interface\n * @param dimensions - Vector dimensions\n * @param vector - Vector capabilities\n */\n static async ensureVectorStorage(\n _db: DatabaseInterface,\n dimensions: number,\n vector: VectorCapabilities,\n ): Promise<void> {\n await vector.ensureColumn('_smrt_embeddings', VECTOR_COLUMN, dimensions);\n await vector.ensureIndex('_smrt_embeddings', VECTOR_COLUMN, {\n dimensions,\n metric: 'cosine',\n type: 'hnsw',\n });\n }\n\n /**\n * Get a specific embedding\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @param fieldName - Field name\n * @param model - Model used\n * @returns Stored embedding or null\n */\n static async get(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n fieldName: string,\n model: string,\n ): Promise<StoredEmbedding | null> {\n const result = await db.single`\n SELECT * FROM _smrt_embeddings\n WHERE object_class = ${objectClass}\n AND object_id = ${objectId}\n AND field_name = ${fieldName}\n AND model = ${model}\n `;\n\n if (!result) return null;\n\n return EmbeddingStorage.parseRow(result);\n }\n\n /**\n * Get all embeddings for an object\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @returns Array of stored embeddings\n */\n static async getForObject(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n ): Promise<StoredEmbedding[]> {\n const { rows } = await db.query(\n `SELECT * FROM _smrt_embeddings WHERE object_class = ? AND object_id = ?`,\n objectClass,\n objectId,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n EmbeddingStorage.parseRow(row),\n );\n }\n\n /**\n * List embeddings for a class (optionally filtered by field and model)\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param fieldName - Optional field filter\n * @param model - Optional model filter\n * @returns Array of stored embeddings\n */\n static async listForClass(\n db: DatabaseInterface,\n objectClass: string,\n fieldName?: string,\n model?: string,\n ): Promise<StoredEmbedding[]> {\n let sql = `SELECT * FROM _smrt_embeddings WHERE object_class = ?`;\n const params: unknown[] = [objectClass];\n\n if (fieldName) {\n sql += ` AND field_name = ?`;\n params.push(fieldName);\n }\n\n if (model) {\n sql += ` AND model = ?`;\n params.push(model);\n }\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n EmbeddingStorage.parseRow(row),\n );\n }\n\n /**\n * Get object IDs that have embeddings for a specific field\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param fieldName - Field name\n * @returns Array of object IDs\n */\n static async getObjectIdsWithEmbeddings(\n db: DatabaseInterface,\n objectClass: string,\n fieldName: string,\n ): Promise<string[]> {\n const { rows } = await db.query(\n `SELECT DISTINCT object_id FROM _smrt_embeddings\n WHERE object_class = ? AND field_name = ?`,\n objectClass,\n fieldName,\n );\n\n return rows.map((row: Record<string, unknown>) => row.object_id as string);\n }\n\n /**\n * Delete all embeddings for an object\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n */\n static async deleteForObject(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n ): Promise<void> {\n await db.query(\n `DELETE FROM _smrt_embeddings WHERE object_class = ? AND object_id = ?`,\n objectClass,\n objectId,\n );\n }\n\n /**\n * Delete a specific embedding\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @param fieldName - Field name\n * @param model - Model used\n */\n static async delete(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n fieldName: string,\n model: string,\n ): Promise<void> {\n await db.query(\n `DELETE FROM _smrt_embeddings\n WHERE object_class = ? AND object_id = ? AND field_name = ? AND model = ?`,\n objectClass,\n objectId,\n fieldName,\n model,\n );\n }\n\n /**\n * Check if embedding table exists\n *\n * @param db - Database interface\n * @returns True if table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_embeddings LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Parse a database row into StoredEmbedding\n */\n private static parseRow(row: Record<string, unknown>): StoredEmbedding {\n return {\n id: row.id as string,\n object_class: row.object_class as string,\n object_id: row.object_id as string,\n field_name: row.field_name as string,\n content_hash: row.content_hash as string,\n embedding: JSON.parse(row.embedding as string) as number[],\n model: row.model as string,\n dimensions: toSafeInteger(row.dimensions, 'Embedding dimensions'),\n provider: row.provider as string | undefined,\n created_at: new Date(row.created_at as string),\n updated_at: new Date(row.updated_at as string),\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"storage.js","names":[],"sources":["../../src/embeddings/storage.ts"],"sourcesContent":["/**\n * Embedding Storage Operations\n *\n * CRUD operations for the _smrt_embeddings system table.\n * Supports both JSON-based and native vector storage strategies.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface, VectorCapabilities } from '@happyvertical/sql';\nimport { chunkArray, IN_LIST_CHUNK_SIZE } from '../utils/chunk';\nimport { toSafeInteger } from '../utils/safe-integer.js';\nimport { CosineSimilarity } from './similarity';\nimport type { StoredEmbedding } from './types';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Column name for native vector storage */\nconst VECTOR_COLUMN = 'embedding_vector';\n\n/**\n * Storage operations for embeddings in _smrt_embeddings table\n */\nexport class EmbeddingStorage {\n /**\n * Insert or update an embedding\n *\n * @param db - Database interface\n * @param data - Embedding data to store\n * @param vector - Optional vector capabilities for native storage\n */\n static async upsert(\n db: DatabaseInterface,\n data: {\n objectClass: string;\n objectId: string;\n fieldName: string;\n contentHash: string;\n embedding: number[];\n model: string;\n dimensions: number;\n provider?: string;\n },\n vector?: VectorCapabilities,\n ): Promise<void> {\n const now = new Date().toISOString();\n const id = globalThis.crypto.randomUUID();\n\n await db.upsert(\n '_smrt_embeddings',\n ['object_class', 'object_id', 'field_name', 'model'],\n {\n id,\n object_class: data.objectClass,\n object_id: data.objectId,\n field_name: data.fieldName,\n content_hash: data.contentHash,\n embedding: JSON.stringify(data.embedding),\n model: data.model,\n dimensions: data.dimensions,\n provider: data.provider || null,\n created_at: now,\n updated_at: now,\n },\n );\n\n // Also store native vector if capability is available\n if (vector) {\n try {\n await vector.upsertVector(\n '_smrt_embeddings',\n {\n object_class: data.objectClass,\n object_id: data.objectId,\n field_name: data.fieldName,\n model: data.model,\n },\n VECTOR_COLUMN,\n data.embedding,\n );\n } catch (error) {\n // Log but don't fail — JSON fallback is always available\n logger.warn(\n `[embeddings] Failed to store native vector: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n /**\n * Search for similar embeddings using the best available strategy.\n *\n * When vector capabilities are provided, delegates to database-level\n * vector search. Otherwise falls back to loading all embeddings and\n * computing cosine similarity in memory.\n *\n * @param db - Database interface\n * @param objectClass - Class name to search within\n * @param embedding - Query embedding vector\n * @param options - Search options\n * @param vector - Optional vector capabilities for native search\n * @returns Array of { objectId, similarity } ranked by similarity (highest first)\n */\n static async searchSimilar(\n db: DatabaseInterface,\n objectClass: string,\n embedding: number[],\n options: {\n field?: string;\n model?: string;\n limit?: number;\n minSimilarity?: number;\n /**\n * Restrict candidates to these object ids BEFORE ranking (#2365).\n * `undefined` means no restriction; an empty array means no candidates\n * (the search returns no results). Used by tenant-scoped collections so\n * top-K never ranks rows the caller is not allowed to see.\n */\n objectIds?: string[];\n } = {},\n vector?: VectorCapabilities,\n ): Promise<Array<{ objectId: string; similarity: number }>> {\n const { field, model, limit = 10, minSimilarity = 0, objectIds } = options;\n\n // An explicit empty candidate set means \"nothing is visible\" — never fall\n // through to the unrestricted search.\n if (objectIds !== undefined && objectIds.length === 0) {\n return [];\n }\n\n // Native vector search path\n if (vector) {\n try {\n // Build WHERE clause for class/field/model filtering.\n // Parameter numbering starts at $2 because vector.search() reserves\n // $1 for the query vector embedding in its internal SQL query.\n const conditions: string[] = ['object_class = $2'];\n const params: unknown[] = [objectClass];\n\n if (field) {\n conditions.push(`field_name = $${params.length + 2}`);\n params.push(field);\n }\n\n if (model) {\n conditions.push(`model = $${params.length + 2}`);\n params.push(model);\n }\n\n // Candidate restriction (#2365). The id list is chunked so one query\n // never exceeds the backend's bind-variable limit; per-chunk top-K\n // results merge into an exact global top-K because every chunk\n // returns its own best `limit` candidates.\n const idChunks = objectIds\n ? chunkArray(objectIds, IN_LIST_CHUNK_SIZE)\n : [undefined];\n\n const merged: Array<{ objectId: string; similarity: number }> = [];\n for (const chunk of idChunks) {\n const chunkConditions = [...conditions];\n const chunkParams = [...params];\n if (chunk) {\n const placeholders = chunk.map(\n (_, index) => `$${chunkParams.length + 2 + index}`,\n );\n chunkConditions.push(`object_id IN (${placeholders.join(', ')})`);\n chunkParams.push(...chunk);\n }\n\n const results = await vector.search(\n '_smrt_embeddings',\n VECTOR_COLUMN,\n embedding,\n {\n limit,\n metric: 'cosine',\n where: chunkConditions.join(' AND '),\n params: chunkParams,\n },\n );\n\n // Convert cosine distance (0=identical, 2=opposite) to similarity\n // (1=identical, -1=opposite)\n merged.push(\n ...results.map((r) => ({\n objectId: r.object_id as string,\n similarity: 1 - r.distance,\n })),\n );\n }\n\n return merged\n .filter((r) => r.similarity >= minSimilarity)\n .sort((a, b) => b.similarity - a.similarity)\n .slice(0, limit);\n } catch (error) {\n // Fall back to in-memory search on vector search failure\n logger.warn(\n `[embeddings] Native vector search failed, falling back to in-memory: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n // In-memory fallback: load all embeddings and compute cosine similarity\n const storedEmbeddings = await EmbeddingStorage.listForClass(\n db,\n objectClass,\n field,\n model,\n );\n\n // Candidate restriction (#2365): drop invisible rows BEFORE ranking so\n // top-K is computed only over what the caller may see.\n const allowedIds = objectIds ? new Set(objectIds) : undefined;\n const candidates = allowedIds\n ? storedEmbeddings.filter((stored) => allowedIds.has(stored.object_id))\n : storedEmbeddings;\n\n if (candidates.length === 0) {\n return [];\n }\n\n return candidates\n .map((stored) => ({\n objectId: stored.object_id,\n similarity: CosineSimilarity.calculate(embedding, stored.embedding),\n }))\n .filter((item) => item.similarity >= minSimilarity)\n .sort((a, b) => b.similarity - a.similarity)\n .slice(0, limit);\n }\n\n /**\n * Exact cosine top-K using bounded embedding pages and a caller-owned scope.\n * JSON vectors are always persisted, including with native vector storage.\n * Eligibility is checked before scoring; neither all vectors nor all eligible\n * object IDs are materialized. The callback must fail closed on scope errors.\n */\n static async searchSimilarBatched(\n db: DatabaseInterface,\n objectClass: string,\n embedding: number[],\n options: {\n field: string;\n model: string;\n limit: number;\n minSimilarity: number;\n eligible: (objectIds: string[]) => Promise<boolean[]>;\n },\n ): Promise<Array<{ objectId: string; similarity: number }>> {\n const batchSize = 64;\n let cursor: string | undefined;\n let best: Array<{ objectId: string; similarity: number }> = [];\n for (;;) {\n const params: unknown[] = [objectClass, options.field, options.model];\n const cursorPredicate = cursor === undefined ? '' : ' AND id > ?';\n if (cursor !== undefined) params.push(cursor);\n const { rows } = await db.query(\n `SELECT CAST(id AS VARCHAR) AS id, CAST(object_id AS VARCHAR) AS object_id, embedding FROM _smrt_embeddings\n WHERE object_class = ? AND field_name = ? AND model = ?${cursorPredicate}\n ORDER BY _smrt_embeddings.id ASC LIMIT ${batchSize}`,\n ...params,\n );\n if (rows.length === 0) break;\n const allowed = await options.eligible(\n rows.map((row: Record<string, unknown>) => String(row.object_id)),\n );\n if (\n allowed.length !== rows.length ||\n allowed.some((value) => typeof value !== 'boolean')\n ) {\n throw new Error('Invalid embedding candidate eligibility mask');\n }\n for (let index = 0; index < rows.length; index++) {\n if (!allowed[index]) continue;\n const row = rows[index];\n const similarity = CosineSimilarity.calculate(\n embedding,\n JSON.parse(row.embedding as string) as number[],\n );\n if (similarity >= options.minSimilarity) {\n best.push({ objectId: String(row.object_id), similarity });\n }\n }\n best.sort(\n (a, b) =>\n b.similarity - a.similarity ||\n (a.objectId < b.objectId ? -1 : a.objectId > b.objectId ? 1 : 0),\n );\n best = best.slice(0, options.limit);\n cursor = String(rows[rows.length - 1].id);\n if (rows.length < batchSize) break;\n }\n return best;\n }\n\n /**\n * Ensure the native vector column and index exist on _smrt_embeddings.\n * Called once during initialization when storage: 'native' is configured.\n *\n * @param db - Database interface\n * @param dimensions - Vector dimensions\n * @param vector - Vector capabilities\n */\n static async ensureVectorStorage(\n _db: DatabaseInterface,\n dimensions: number,\n vector: VectorCapabilities,\n ): Promise<void> {\n await vector.ensureColumn('_smrt_embeddings', VECTOR_COLUMN, dimensions);\n await vector.ensureIndex('_smrt_embeddings', VECTOR_COLUMN, {\n dimensions,\n metric: 'cosine',\n type: 'hnsw',\n });\n }\n\n /**\n * Get a specific embedding\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @param fieldName - Field name\n * @param model - Model used\n * @returns Stored embedding or null\n */\n static async get(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n fieldName: string,\n model: string,\n ): Promise<StoredEmbedding | null> {\n const result = await db.single`\n SELECT * FROM _smrt_embeddings\n WHERE object_class = ${objectClass}\n AND object_id = ${objectId}\n AND field_name = ${fieldName}\n AND model = ${model}\n `;\n\n if (!result) return null;\n\n return EmbeddingStorage.parseRow(result);\n }\n\n /**\n * Get all embeddings for an object\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @returns Array of stored embeddings\n */\n static async getForObject(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n ): Promise<StoredEmbedding[]> {\n const { rows } = await db.query(\n `SELECT * FROM _smrt_embeddings WHERE object_class = ? AND object_id = ?`,\n objectClass,\n objectId,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n EmbeddingStorage.parseRow(row),\n );\n }\n\n /**\n * List embeddings for a class (optionally filtered by field and model)\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param fieldName - Optional field filter\n * @param model - Optional model filter\n * @returns Array of stored embeddings\n */\n static async listForClass(\n db: DatabaseInterface,\n objectClass: string,\n fieldName?: string,\n model?: string,\n ): Promise<StoredEmbedding[]> {\n let sql = `SELECT * FROM _smrt_embeddings WHERE object_class = ?`;\n const params: unknown[] = [objectClass];\n\n if (fieldName) {\n sql += ` AND field_name = ?`;\n params.push(fieldName);\n }\n\n if (model) {\n sql += ` AND model = ?`;\n params.push(model);\n }\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n EmbeddingStorage.parseRow(row),\n );\n }\n\n /**\n * Get object IDs that have embeddings for a specific field\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param fieldName - Field name\n * @returns Array of object IDs\n */\n static async getObjectIdsWithEmbeddings(\n db: DatabaseInterface,\n objectClass: string,\n fieldName: string,\n ): Promise<string[]> {\n const { rows } = await db.query(\n `SELECT DISTINCT object_id FROM _smrt_embeddings\n WHERE object_class = ? AND field_name = ?`,\n objectClass,\n fieldName,\n );\n\n return rows.map((row: Record<string, unknown>) => row.object_id as string);\n }\n\n /**\n * Delete all embeddings for an object\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n */\n static async deleteForObject(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n ): Promise<void> {\n await db.query(\n `DELETE FROM _smrt_embeddings WHERE object_class = ? AND object_id = ?`,\n objectClass,\n objectId,\n );\n }\n\n /**\n * Delete a specific embedding\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @param fieldName - Field name\n * @param model - Model used\n */\n static async delete(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n fieldName: string,\n model: string,\n ): Promise<void> {\n await db.query(\n `DELETE FROM _smrt_embeddings\n WHERE object_class = ? AND object_id = ? AND field_name = ? AND model = ?`,\n objectClass,\n objectId,\n fieldName,\n model,\n );\n }\n\n /**\n * Check if embedding table exists\n *\n * @param db - Database interface\n * @returns True if table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_embeddings LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Parse a database row into StoredEmbedding\n */\n private static parseRow(row: Record<string, unknown>): StoredEmbedding {\n return {\n id: row.id as string,\n object_class: row.object_class as string,\n object_id: row.object_id as string,\n field_name: row.field_name as string,\n content_hash: row.content_hash as string,\n embedding: JSON.parse(row.embedding as string) as number[],\n model: row.model as string,\n dimensions: toSafeInteger(row.dimensions, 'Embedding dimensions'),\n provider: row.provider as string | undefined,\n created_at: new Date(row.created_at as string),\n updated_at: new Date(row.updated_at as string),\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
package/dist/errors.js CHANGED
@@ -562,6 +562,7 @@ var ErrorUtils = class {
562
562
  lastError = error instanceof Error ? error : new Error(String(error));
563
563
  if (attempt === maxRetries) throw lastError;
564
564
  if (error instanceof ValidationError || error instanceof ConfigurationError || error instanceof TenantIsolationError) throw error;
565
+ if (error instanceof RuntimeError && error.code === "RUNTIME_INVALID_STATE") throw error;
565
566
  if (classifyDatabaseError(error).deterministic) throw lastError;
566
567
  try {
567
568
  await new Promise((resolve) => {