@happyvertical/smrt-core 0.41.0 → 0.42.1
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.
- package/AGENTS.md +1 -1
- package/agents/schema-paths.md +79 -8
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +2 -2
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts.map +1 -1
- package/dist/change-signals.js +8 -1
- package/dist/change-signals.js.map +1 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +10 -9
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +2 -2
- package/dist/collection.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +5 -5
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/models/Dispatch.d.ts +1 -1
- package/dist/dispatch/models/Dispatch.d.ts.map +1 -1
- package/dist/dispatch/models/Dispatch.js +3 -2
- package/dist/dispatch/models/Dispatch.js.map +1 -1
- package/dist/dispatch/models/DispatchSubscription.d.ts +1 -1
- package/dist/dispatch/models/DispatchSubscription.d.ts.map +1 -1
- package/dist/dispatch/models/DispatchSubscription.js +2 -1
- package/dist/dispatch/models/DispatchSubscription.js.map +1 -1
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +2 -1
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/learning/memory.d.ts.map +1 -1
- package/dist/learning/memory.js +5 -4
- package/dist/learning/memory.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.js +2 -2
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -1
- package/dist/migrations/integer-width.d.ts +99 -0
- package/dist/migrations/integer-width.d.ts.map +1 -0
- package/dist/migrations/integer-width.js +237 -0
- package/dist/migrations/integer-width.js.map +1 -0
- package/dist/migrations/minor-units.d.ts +2 -2
- package/dist/migrations/minor-units.d.ts.map +1 -1
- package/dist/migrations/minor-units.js +16 -14
- package/dist/migrations/minor-units.js.map +1 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +28 -20
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations.js +2 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +1 -0
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +2 -0
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +1 -33
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +1 -150
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index.d.ts +1 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -2
- package/dist/schema/live-parity.d.ts +3 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +19 -0
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/schema/types.d.ts +17 -10
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema.js +1 -2
- package/dist/smrt-knowledge.json +7 -7
- package/dist/system/retention.d.ts.map +1 -1
- package/dist/system/retention.js +3 -3
- package/dist/system/retention.js.map +1 -1
- package/dist/system/schema.d.ts +2 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +4 -2
- package/dist/system/schema.js.map +1 -1
- package/dist/utils/safe-integer.d.ts +19 -0
- package/dist/utils/safe-integer.d.ts.map +1 -0
- package/dist/utils/safe-integer.js +31 -0
- package/dist/utils/safe-integer.js.map +1 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -3
- package/dist/utils.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +0 -53
- package/dist/vite-plugin/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/schema/override-system.d.ts +0 -43
- package/dist/schema/override-system.d.ts.map +0 -1
- package/dist/schema/override-system.js +0 -205
- package/dist/schema/override-system.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dispatches.d.ts","sourceRoot":"","sources":["../../../src/dispatch/collections/Dispatches.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"Dispatches.d.ts","sourceRoot":"","sources":["../../../src/dispatch/collections/Dispatches.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,EAAE,QAAQ,EAAqB,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACf,MAAM,aAAa,CAAC;AAoErB;;GAEG;AACH,qBAAa,kBAAkB;IAC7B;;OAEG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC;IA0BhB;;OAEG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC;IAyBhB;;;;;;OAMG;WACU,GAAG,CACd,EAAE,EAAE,iBAAiB,EACrB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAS3B;;;;;;OAMG;WACU,SAAS,CACpB,EAAE,EAAE,iBAAiB,EACrB,EAAE,EAAE,MAAM,EACV,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAe3B;;OAEG;WACU,IAAI,CACf,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,EAAE,CAAC;IAuFtB;;;;;;;;;;OAUG;WACU,WAAW,CACtB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,GAAE,MAAY,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,QAAQ,EAAE,CAAC;IAsCtB;;;;;;;;;;;;;;;;OAgBG;WACU,KAAK,CAChB,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,QAAQ,EAClB,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,OAAO,CAAC;IA6BnB;;;;;;;OAOG;WACU,aAAa,CACxB,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,oBAAyB,EAClC,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,QAAQ,EAAE,CAAC;IAuCtB;;OAEG;WACU,aAAa,CACxB,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC;IAUlB;;OAEG;WACU,MAAM,CAAC,EAAE,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrE;;;;;;;OAOG;WACU,OAAO,CAClB,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,sBAA2B,EACpC,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,qBAAqB,CAAC;IA4CjC;;OAEG;WACU,WAAW,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;CAQlE"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toSafeInteger } from "../../utils/safe-integer.js";
|
|
1
2
|
import { Dispatch } from "../models/Dispatch.js";
|
|
2
3
|
//#region src/dispatch/collections/Dispatches.ts
|
|
3
4
|
/**
|
|
@@ -36,9 +37,8 @@ function pushTenantPredicate(conditions, params, scope, placeholder) {
|
|
|
36
37
|
*/
|
|
37
38
|
async function deleteOrCount(db, conditions, params, dryRun) {
|
|
38
39
|
const where = conditions.join(" AND ");
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
if (!Number.isFinite(total) || total <= 0) return 0;
|
|
40
|
+
const total = toSafeInteger((await db.query(`SELECT COUNT(*) AS total FROM _smrt_dispatch WHERE ${where}`, ...params)).rows?.[0]?.total ?? 0, "Dispatch cleanup count");
|
|
41
|
+
if (total <= 0) return 0;
|
|
42
42
|
if (!dryRun) await db.query(`DELETE FROM _smrt_dispatch WHERE ${where}`, ...params);
|
|
43
43
|
return total;
|
|
44
44
|
}
|
|
@@ -248,9 +248,9 @@ var DispatchCollection = class {
|
|
|
248
248
|
* Count dispatches by status
|
|
249
249
|
*/
|
|
250
250
|
static async countByStatus(db, status) {
|
|
251
|
-
return (await db.single`
|
|
251
|
+
return toSafeInteger((await db.single`
|
|
252
252
|
SELECT COUNT(*) as count FROM _smrt_dispatch WHERE status = ${status}
|
|
253
|
-
`)?.count
|
|
253
|
+
`)?.count ?? 0, "Dispatch count");
|
|
254
254
|
}
|
|
255
255
|
/**
|
|
256
256
|
* Delete a dispatch
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dispatches.js","names":[],"sources":["../../../src/dispatch/collections/Dispatches.ts"],"sourcesContent":["/**\n * DispatchCollection - Storage operations for dispatch messages\n *\n * CRUD operations for the _smrt_dispatch system table.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { Dispatch, type DispatchData } from '../models/Dispatch.js';\nimport type { DispatchTenantScope } from '../tenant-resolver.js';\nimport type {\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchListOptions,\n DispatchRetryOptions,\n DispatchStatus,\n} from '../types.js';\n\n/**\n * Build the SQL tenant predicate for the active scope (S5 #1398).\n *\n * Appends a parameterized condition to `conditions` (and the corresponding\n * value to `params`) based on the resolved {@link DispatchTenantScope}:\n *\n * - tenancy off / no scope → no predicate (pre-tenancy behavior)\n * - `enforced` + active tenant T → `(tenant_id = ? OR tenant_id IS NULL)`\n * - `enforced` + no active tenant → `tenant_id IS NULL` (fail-closed global)\n *\n * @param placeholder - Engine-specific positional placeholder (e.g. `$3`) used\n * when a tenant id value is bound. Ignored for the no-value branches.\n * @returns `true` when a value placeholder was consumed (so the caller can\n * advance its param index), `false` otherwise.\n */\nfunction pushTenantPredicate(\n conditions: string[],\n params: unknown[],\n scope: DispatchTenantScope | undefined,\n placeholder: string,\n): boolean {\n if (!scope?.enforced) {\n return false;\n }\n if (scope.tenantId !== null) {\n conditions.push(`(tenant_id = ${placeholder} OR tenant_id IS NULL)`);\n params.push(scope.tenantId);\n return true;\n }\n conditions.push('tenant_id IS NULL');\n return false;\n}\n\n/**\n * Count the rows a cleanup predicate selects, then delete them (#2375).\n *\n * Counting first gives a usable figure on every adapter — `rowCount` is not\n * reliably populated across the engines SMRT supports — and is what lets\n * `dryRun` preview the very same predicate the real cleanup runs. The two\n * statements are not transactional, so the figure is approximate under\n * concurrent writers.\n */\nasync function deleteOrCount(\n db: DatabaseInterface,\n conditions: string[],\n params: unknown[],\n dryRun: boolean,\n): Promise<number> {\n const where = conditions.join(' AND ');\n const countResult = await db.query(\n `SELECT COUNT(*) AS total FROM _smrt_dispatch WHERE ${where}`,\n ...params,\n );\n const total = Number(countResult.rows?.[0]?.total ?? 0);\n if (!Number.isFinite(total) || total <= 0) return 0;\n\n if (!dryRun) {\n await db.query(`DELETE FROM _smrt_dispatch WHERE ${where}`, ...params);\n }\n\n return total;\n}\n\n/**\n * Storage operations for dispatches in _smrt_dispatch table\n */\nexport class DispatchCollection {\n /**\n * Insert a new dispatch\n */\n static async insert(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `INSERT INTO _smrt_dispatch\n (id, type, source, source_id, payload, status, attempts, last_error,\n processed_at, processed_by, target_subscriber, correlation_id, tenant_id, metadata, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,\n row.id,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.tenant_id,\n row.metadata,\n row.created_at,\n row.updated_at,\n );\n }\n\n /**\n * Update an existing dispatch\n */\n static async update(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `UPDATE _smrt_dispatch SET\n type = $1, source = $2, source_id = $3, payload = $4, status = $5,\n attempts = $6, last_error = $7, processed_at = $8, processed_by = $9,\n target_subscriber = $10, correlation_id = $11, metadata = $12, updated_at = $13\n WHERE id = $14`,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.metadata,\n row.updated_at,\n row.id,\n );\n }\n\n /**\n * Get a dispatch by ID (unscoped).\n *\n * Internal/system use only — applies no tenant filter. Public reads go\n * through the DispatchBus, which uses {@link getScoped} so a caller cannot\n * fetch another tenant's dispatch by id (S5 #1398).\n */\n static async get(\n db: DatabaseInterface,\n id: string,\n ): Promise<Dispatch | null> {\n const result = await db.single`\n SELECT * FROM _smrt_dispatch WHERE id = ${id}\n `;\n\n if (!result) return null;\n return Dispatch.fromRow(result as DispatchData);\n }\n\n /**\n * Get a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same tenant predicate as {@link list}/{@link findPending}, so a\n * cross-tenant or out-of-scope id lookup returns `null` rather than leaking\n * the row. The scope is derived server-side by the DispatchBus.\n */\n static async getScoped(\n db: DatabaseInterface,\n id: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch | null> {\n const conditions: string[] = ['id = $1'];\n const params: unknown[] = [id];\n\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} LIMIT 1`,\n ...params,\n );\n\n if (rows.length === 0) return null;\n return Dispatch.fromRow(rows[0] as unknown as DispatchData);\n }\n\n /**\n * List dispatches with filtering\n */\n static async list(\n db: DatabaseInterface,\n options: DispatchListOptions = {},\n ): Promise<Dispatch[]> {\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.status) {\n conditions.push(`status = $${paramIndex++}`);\n params.push(options.status);\n }\n\n if (options.source) {\n conditions.push(`source = $${paramIndex++}`);\n params.push(options.source);\n }\n\n if (options.type) {\n conditions.push(`type = $${paramIndex++}`);\n params.push(options.type);\n }\n\n if (options.targetSubscriber) {\n conditions.push(`target_subscriber = $${paramIndex++}`);\n params.push(options.targetSubscriber);\n }\n\n if (options.correlationId !== undefined) {\n conditions.push(`correlation_id = $${paramIndex++}`);\n params.push(options.correlationId);\n }\n\n // Tenant isolation (S5 #1398). Scope is derived server-side by the bus;\n // callers cannot widen it. See DispatchListOptions.tenantScope.\n if (\n pushTenantPredicate(\n conditions,\n params,\n options.tenantScope,\n `$${paramIndex}`,\n )\n ) {\n paramIndex++;\n }\n\n let sql = 'SELECT * FROM _smrt_dispatch';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n // Whitelist allowed orderBy values to prevent SQL injection\n const allowedOrderBy = [\n 'created_at DESC',\n 'created_at ASC',\n 'updated_at DESC',\n 'updated_at ASC',\n 'type',\n 'type DESC',\n 'type ASC',\n 'status',\n 'status DESC',\n 'status ASC',\n 'source',\n 'source DESC',\n 'source ASC',\n ];\n const safeOrderBy =\n options.orderBy && allowedOrderBy.includes(options.orderBy)\n ? options.orderBy\n : 'created_at DESC';\n sql += ` ORDER BY ${safeOrderBy}`;\n\n // Use parameterized queries for limit/offset\n if (options.limit !== undefined) {\n sql += ` LIMIT $${paramIndex++}`;\n params.push(options.limit);\n }\n\n if (options.offset !== undefined) {\n sql += ` OFFSET $${paramIndex++}`;\n params.push(options.offset);\n }\n\n const { rows } = await db.query(sql, ...params);\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Find pending dispatches for a set of signal types\n *\n * @param subscriber - When provided, filters to dispatches with no target\n * or targeted specifically at this subscriber (for fan-out delivery)\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts results so a subscriber in one tenant cannot see\n * another tenant's dispatch. See {@link pushTenantPredicate} for the exact\n * semantics (off → no filter; active tenant → that tenant + global; on but\n * no tenant → global only).\n */\n static async findPending(\n db: DatabaseInterface,\n signalTypes: string[],\n limit: number = 100,\n subscriber?: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n if (signalTypes.length === 0) {\n return [];\n }\n\n let paramIndex = 1;\n const placeholders = signalTypes.map(() => `$${paramIndex++}`).join(', ');\n const conditions: string[] = [\n \"status = 'pending'\",\n `type IN (${placeholders})`,\n ];\n const params: unknown[] = [...signalTypes];\n\n if (subscriber) {\n conditions.push(\n `(target_subscriber IS NULL OR target_subscriber = $${paramIndex++})`,\n );\n params.push(subscriber);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const sql = `SELECT * FROM _smrt_dispatch\n WHERE ${conditions.join(' AND ')}\n ORDER BY created_at ASC LIMIT $${paramIndex}`;\n params.push(limit);\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Atomically claim a pending dispatch for processing (S5 #1398).\n *\n * Performs a conditional `UPDATE ... WHERE id = ? AND status = 'pending'`\n * (plus the tenant predicate for the active scope) and reports whether the\n * row was claimed by THIS call. Only the caller whose update affected a row\n * may run the handler, which closes the claim TOCTOU window where a `process`\n * filter-then-update could let two workers (or a tenant-scoped and a\n * system-scoped processor) both pick up the same compete/global dispatch.\n *\n * On a successful claim the dispatch's status/attempts/updated_at are\n * advanced in the DB; the in-memory `dispatch` instance is updated to match\n * so the caller can proceed to `update()` it to completed/failed.\n *\n * @returns `true` if this call claimed the dispatch, `false` if another\n * worker already moved it out of `pending` (or it is not visible in scope).\n */\n static async claim(\n db: DatabaseInterface,\n dispatch: Dispatch,\n tenantScope?: DispatchTenantScope,\n ): Promise<boolean> {\n dispatch.markProcessing();\n const row = dispatch.toRow();\n\n const conditions: string[] = ['id = $4', \"status = 'pending'\"];\n const params: unknown[] = [\n row.status,\n row.attempts,\n row.updated_at,\n row.id,\n ];\n let paramIndex = 5;\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rowCount } = await db.query(\n `UPDATE _smrt_dispatch\n SET status = $1, attempts = $2, updated_at = $3\n WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n\n return (rowCount ?? 0) > 0;\n }\n\n /**\n * Find failed dispatches eligible for retry.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts retryable rows to the active tenant (plus global) so a\n * tenant's `retry()` cannot reset another tenant's failed dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async findRetryable(\n db: DatabaseInterface,\n options: DispatchRetryOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n const conditions: string[] = [\"status = 'failed'\"];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.maxAttempts) {\n conditions.push(`attempts < $${paramIndex++}`);\n params.push(options.maxAttempts);\n }\n\n if (options.olderThan) {\n conditions.push(`created_at < $${paramIndex++}`);\n params.push(options.olderThan.toISOString());\n }\n\n if (options.signalTypes && options.signalTypes.length > 0) {\n const placeholders = options.signalTypes\n .map(() => `$${paramIndex++}`)\n .join(', ');\n conditions.push(`type IN (${placeholders})`);\n params.push(...options.signalTypes);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} ORDER BY created_at ASC`,\n ...params,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Count dispatches by status\n */\n static async countByStatus(\n db: DatabaseInterface,\n status: DispatchStatus,\n ): Promise<number> {\n const result = await db.single`\n SELECT COUNT(*) as count FROM _smrt_dispatch WHERE status = ${status}\n `;\n return (result as { count: number })?.count || 0;\n }\n\n /**\n * Delete a dispatch\n */\n static async delete(db: DatabaseInterface, id: string): Promise<void> {\n await db.query(`DELETE FROM _smrt_dispatch WHERE id = $1`, id);\n }\n\n /**\n * Cleanup old dispatches.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts deletions to the active tenant (plus global) so a\n * tenant's `cleanup()` cannot delete another tenant's dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async cleanup(\n db: DatabaseInterface,\n options: DispatchCleanupOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<DispatchCleanupResult> {\n const result: DispatchCleanupResult = {\n completedDeleted: 0,\n failedDeleted: 0,\n };\n\n if (options.completedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.completedOlderThanDays);\n\n const conditions: string[] = [\n \"status = 'completed'\",\n 'processed_at < $1',\n ];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n result.completedDeleted = await deleteOrCount(\n db,\n conditions,\n params,\n options.dryRun ?? false,\n );\n }\n\n if (options.failedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.failedOlderThanDays);\n\n const conditions: string[] = [\"status = 'failed'\", 'updated_at < $1'];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n result.failedDeleted = await deleteOrCount(\n db,\n conditions,\n params,\n options.dryRun ?? false,\n );\n }\n\n return result;\n }\n\n /**\n * Check if dispatch table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_dispatch LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Dispatches.js","names":[],"sources":["../../../src/dispatch/collections/Dispatches.ts"],"sourcesContent":["/**\n * DispatchCollection - Storage operations for dispatch messages\n *\n * CRUD operations for the _smrt_dispatch system table.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { toSafeInteger } from '../../utils/safe-integer.js';\nimport { Dispatch, type DispatchData } from '../models/Dispatch.js';\nimport type { DispatchTenantScope } from '../tenant-resolver.js';\nimport type {\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchListOptions,\n DispatchRetryOptions,\n DispatchStatus,\n} from '../types.js';\n\n/**\n * Build the SQL tenant predicate for the active scope (S5 #1398).\n *\n * Appends a parameterized condition to `conditions` (and the corresponding\n * value to `params`) based on the resolved {@link DispatchTenantScope}:\n *\n * - tenancy off / no scope → no predicate (pre-tenancy behavior)\n * - `enforced` + active tenant T → `(tenant_id = ? OR tenant_id IS NULL)`\n * - `enforced` + no active tenant → `tenant_id IS NULL` (fail-closed global)\n *\n * @param placeholder - Engine-specific positional placeholder (e.g. `$3`) used\n * when a tenant id value is bound. Ignored for the no-value branches.\n * @returns `true` when a value placeholder was consumed (so the caller can\n * advance its param index), `false` otherwise.\n */\nfunction pushTenantPredicate(\n conditions: string[],\n params: unknown[],\n scope: DispatchTenantScope | undefined,\n placeholder: string,\n): boolean {\n if (!scope?.enforced) {\n return false;\n }\n if (scope.tenantId !== null) {\n conditions.push(`(tenant_id = ${placeholder} OR tenant_id IS NULL)`);\n params.push(scope.tenantId);\n return true;\n }\n conditions.push('tenant_id IS NULL');\n return false;\n}\n\n/**\n * Count the rows a cleanup predicate selects, then delete them (#2375).\n *\n * Counting first gives a usable figure on every adapter — `rowCount` is not\n * reliably populated across the engines SMRT supports — and is what lets\n * `dryRun` preview the very same predicate the real cleanup runs. The two\n * statements are not transactional, so the figure is approximate under\n * concurrent writers.\n */\nasync function deleteOrCount(\n db: DatabaseInterface,\n conditions: string[],\n params: unknown[],\n dryRun: boolean,\n): Promise<number> {\n const where = conditions.join(' AND ');\n const countResult = await db.query(\n `SELECT COUNT(*) AS total FROM _smrt_dispatch WHERE ${where}`,\n ...params,\n );\n const total = toSafeInteger(\n countResult.rows?.[0]?.total ?? 0,\n 'Dispatch cleanup count',\n );\n if (total <= 0) return 0;\n\n if (!dryRun) {\n await db.query(`DELETE FROM _smrt_dispatch WHERE ${where}`, ...params);\n }\n\n return total;\n}\n\n/**\n * Storage operations for dispatches in _smrt_dispatch table\n */\nexport class DispatchCollection {\n /**\n * Insert a new dispatch\n */\n static async insert(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `INSERT INTO _smrt_dispatch\n (id, type, source, source_id, payload, status, attempts, last_error,\n processed_at, processed_by, target_subscriber, correlation_id, tenant_id, metadata, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,\n row.id,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.tenant_id,\n row.metadata,\n row.created_at,\n row.updated_at,\n );\n }\n\n /**\n * Update an existing dispatch\n */\n static async update(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `UPDATE _smrt_dispatch SET\n type = $1, source = $2, source_id = $3, payload = $4, status = $5,\n attempts = $6, last_error = $7, processed_at = $8, processed_by = $9,\n target_subscriber = $10, correlation_id = $11, metadata = $12, updated_at = $13\n WHERE id = $14`,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.metadata,\n row.updated_at,\n row.id,\n );\n }\n\n /**\n * Get a dispatch by ID (unscoped).\n *\n * Internal/system use only — applies no tenant filter. Public reads go\n * through the DispatchBus, which uses {@link getScoped} so a caller cannot\n * fetch another tenant's dispatch by id (S5 #1398).\n */\n static async get(\n db: DatabaseInterface,\n id: string,\n ): Promise<Dispatch | null> {\n const result = await db.single`\n SELECT * FROM _smrt_dispatch WHERE id = ${id}\n `;\n\n if (!result) return null;\n return Dispatch.fromRow(result as DispatchData);\n }\n\n /**\n * Get a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same tenant predicate as {@link list}/{@link findPending}, so a\n * cross-tenant or out-of-scope id lookup returns `null` rather than leaking\n * the row. The scope is derived server-side by the DispatchBus.\n */\n static async getScoped(\n db: DatabaseInterface,\n id: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch | null> {\n const conditions: string[] = ['id = $1'];\n const params: unknown[] = [id];\n\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} LIMIT 1`,\n ...params,\n );\n\n if (rows.length === 0) return null;\n return Dispatch.fromRow(rows[0] as unknown as DispatchData);\n }\n\n /**\n * List dispatches with filtering\n */\n static async list(\n db: DatabaseInterface,\n options: DispatchListOptions = {},\n ): Promise<Dispatch[]> {\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.status) {\n conditions.push(`status = $${paramIndex++}`);\n params.push(options.status);\n }\n\n if (options.source) {\n conditions.push(`source = $${paramIndex++}`);\n params.push(options.source);\n }\n\n if (options.type) {\n conditions.push(`type = $${paramIndex++}`);\n params.push(options.type);\n }\n\n if (options.targetSubscriber) {\n conditions.push(`target_subscriber = $${paramIndex++}`);\n params.push(options.targetSubscriber);\n }\n\n if (options.correlationId !== undefined) {\n conditions.push(`correlation_id = $${paramIndex++}`);\n params.push(options.correlationId);\n }\n\n // Tenant isolation (S5 #1398). Scope is derived server-side by the bus;\n // callers cannot widen it. See DispatchListOptions.tenantScope.\n if (\n pushTenantPredicate(\n conditions,\n params,\n options.tenantScope,\n `$${paramIndex}`,\n )\n ) {\n paramIndex++;\n }\n\n let sql = 'SELECT * FROM _smrt_dispatch';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n // Whitelist allowed orderBy values to prevent SQL injection\n const allowedOrderBy = [\n 'created_at DESC',\n 'created_at ASC',\n 'updated_at DESC',\n 'updated_at ASC',\n 'type',\n 'type DESC',\n 'type ASC',\n 'status',\n 'status DESC',\n 'status ASC',\n 'source',\n 'source DESC',\n 'source ASC',\n ];\n const safeOrderBy =\n options.orderBy && allowedOrderBy.includes(options.orderBy)\n ? options.orderBy\n : 'created_at DESC';\n sql += ` ORDER BY ${safeOrderBy}`;\n\n // Use parameterized queries for limit/offset\n if (options.limit !== undefined) {\n sql += ` LIMIT $${paramIndex++}`;\n params.push(options.limit);\n }\n\n if (options.offset !== undefined) {\n sql += ` OFFSET $${paramIndex++}`;\n params.push(options.offset);\n }\n\n const { rows } = await db.query(sql, ...params);\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Find pending dispatches for a set of signal types\n *\n * @param subscriber - When provided, filters to dispatches with no target\n * or targeted specifically at this subscriber (for fan-out delivery)\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts results so a subscriber in one tenant cannot see\n * another tenant's dispatch. See {@link pushTenantPredicate} for the exact\n * semantics (off → no filter; active tenant → that tenant + global; on but\n * no tenant → global only).\n */\n static async findPending(\n db: DatabaseInterface,\n signalTypes: string[],\n limit: number = 100,\n subscriber?: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n if (signalTypes.length === 0) {\n return [];\n }\n\n let paramIndex = 1;\n const placeholders = signalTypes.map(() => `$${paramIndex++}`).join(', ');\n const conditions: string[] = [\n \"status = 'pending'\",\n `type IN (${placeholders})`,\n ];\n const params: unknown[] = [...signalTypes];\n\n if (subscriber) {\n conditions.push(\n `(target_subscriber IS NULL OR target_subscriber = $${paramIndex++})`,\n );\n params.push(subscriber);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const sql = `SELECT * FROM _smrt_dispatch\n WHERE ${conditions.join(' AND ')}\n ORDER BY created_at ASC LIMIT $${paramIndex}`;\n params.push(limit);\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Atomically claim a pending dispatch for processing (S5 #1398).\n *\n * Performs a conditional `UPDATE ... WHERE id = ? AND status = 'pending'`\n * (plus the tenant predicate for the active scope) and reports whether the\n * row was claimed by THIS call. Only the caller whose update affected a row\n * may run the handler, which closes the claim TOCTOU window where a `process`\n * filter-then-update could let two workers (or a tenant-scoped and a\n * system-scoped processor) both pick up the same compete/global dispatch.\n *\n * On a successful claim the dispatch's status/attempts/updated_at are\n * advanced in the DB; the in-memory `dispatch` instance is updated to match\n * so the caller can proceed to `update()` it to completed/failed.\n *\n * @returns `true` if this call claimed the dispatch, `false` if another\n * worker already moved it out of `pending` (or it is not visible in scope).\n */\n static async claim(\n db: DatabaseInterface,\n dispatch: Dispatch,\n tenantScope?: DispatchTenantScope,\n ): Promise<boolean> {\n dispatch.markProcessing();\n const row = dispatch.toRow();\n\n const conditions: string[] = ['id = $4', \"status = 'pending'\"];\n const params: unknown[] = [\n row.status,\n row.attempts,\n row.updated_at,\n row.id,\n ];\n let paramIndex = 5;\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rowCount } = await db.query(\n `UPDATE _smrt_dispatch\n SET status = $1, attempts = $2, updated_at = $3\n WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n\n return (rowCount ?? 0) > 0;\n }\n\n /**\n * Find failed dispatches eligible for retry.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts retryable rows to the active tenant (plus global) so a\n * tenant's `retry()` cannot reset another tenant's failed dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async findRetryable(\n db: DatabaseInterface,\n options: DispatchRetryOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n const conditions: string[] = [\"status = 'failed'\"];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.maxAttempts) {\n conditions.push(`attempts < $${paramIndex++}`);\n params.push(options.maxAttempts);\n }\n\n if (options.olderThan) {\n conditions.push(`created_at < $${paramIndex++}`);\n params.push(options.olderThan.toISOString());\n }\n\n if (options.signalTypes && options.signalTypes.length > 0) {\n const placeholders = options.signalTypes\n .map(() => `$${paramIndex++}`)\n .join(', ');\n conditions.push(`type IN (${placeholders})`);\n params.push(...options.signalTypes);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} ORDER BY created_at ASC`,\n ...params,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Count dispatches by status\n */\n static async countByStatus(\n db: DatabaseInterface,\n status: DispatchStatus,\n ): Promise<number> {\n const result = await db.single`\n SELECT COUNT(*) as count FROM _smrt_dispatch WHERE status = ${status}\n `;\n return toSafeInteger(\n (result as { count?: unknown } | null)?.count ?? 0,\n 'Dispatch count',\n );\n }\n\n /**\n * Delete a dispatch\n */\n static async delete(db: DatabaseInterface, id: string): Promise<void> {\n await db.query(`DELETE FROM _smrt_dispatch WHERE id = $1`, id);\n }\n\n /**\n * Cleanup old dispatches.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts deletions to the active tenant (plus global) so a\n * tenant's `cleanup()` cannot delete another tenant's dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async cleanup(\n db: DatabaseInterface,\n options: DispatchCleanupOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<DispatchCleanupResult> {\n const result: DispatchCleanupResult = {\n completedDeleted: 0,\n failedDeleted: 0,\n };\n\n if (options.completedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.completedOlderThanDays);\n\n const conditions: string[] = [\n \"status = 'completed'\",\n 'processed_at < $1',\n ];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n result.completedDeleted = await deleteOrCount(\n db,\n conditions,\n params,\n options.dryRun ?? false,\n );\n }\n\n if (options.failedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.failedOlderThanDays);\n\n const conditions: string[] = [\"status = 'failed'\", 'updated_at < $1'];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n result.failedDeleted = await deleteOrCount(\n db,\n conditions,\n params,\n options.dryRun ?? false,\n );\n }\n\n return result;\n }\n\n /**\n * Check if dispatch table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_dispatch LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ export interface DispatchData {
|
|
|
9
9
|
source_id: string | null;
|
|
10
10
|
payload: string | null;
|
|
11
11
|
status: DispatchStatus;
|
|
12
|
-
attempts: number;
|
|
12
|
+
attempts: number | string | bigint;
|
|
13
13
|
last_error: string | null;
|
|
14
14
|
processed_at: string | null;
|
|
15
15
|
processed_by: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dispatch.d.ts","sourceRoot":"","sources":["../../../src/dispatch/models/Dispatch.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"Dispatch.d.ts","sourceRoot":"","sources":["../../../src/dispatch/models/Dispatch.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgCpE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,gCAAgC;IAChC,MAAM,CAAC,QAAQ,CAAC,SAAS,oBAAoB;IAE7C,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IAEX,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IAEb,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IAEf,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IAEjB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEjC,qBAAqB;IACrB,MAAM,EAAE,cAAc,CAAC;IAEvB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IAEjB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAElB,sCAAsC;IACtC,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IAEzB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IAEpB,qEAAqE;IACrE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhC,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IAEtB;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElC,yBAAyB;IACzB,SAAS,EAAE,IAAI,CAAC;IAEhB,4BAA4B;IAC5B,SAAS,EAAE,IAAI,CAAC;gBAEJ,IAAI,GAAE,OAAO,CAAC,YAAY,CAAM;IA0B5C;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,QAAQ;IAI3C;;OAEG;IACH,KAAK,IAAI,YAAY;IAqBrB;;OAEG;IACH,WAAW,IAAI,gBAAgB;IAa/B;;OAEG;IACH,cAAc,IAAI,IAAI;IAMtB;;OAEG;IACH,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAQxC;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM/B;;OAEG;IACH,aAAa,IAAI,IAAI;IAKrB;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;CAGvC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toSafeInteger } from "../../utils/safe-integer.js";
|
|
1
2
|
import { createLogger } from "@happyvertical/logger";
|
|
2
3
|
import { makeId } from "@happyvertical/utils";
|
|
3
4
|
//#region src/dispatch/models/Dispatch.ts
|
|
@@ -79,7 +80,7 @@ var Dispatch = class Dispatch {
|
|
|
79
80
|
this.source = data.source || "";
|
|
80
81
|
this.sourceId = data.source_id || "";
|
|
81
82
|
this.status = data.status || "pending";
|
|
82
|
-
this.attempts = data.attempts
|
|
83
|
+
this.attempts = toSafeInteger(data.attempts ?? 0, "Dispatch attempts");
|
|
83
84
|
this.lastError = data.last_error || "";
|
|
84
85
|
this.processedBy = data.processed_by || "";
|
|
85
86
|
this.targetSubscriber = data.target_subscriber || null;
|
|
@@ -140,7 +141,7 @@ var Dispatch = class Dispatch {
|
|
|
140
141
|
*/
|
|
141
142
|
markProcessing() {
|
|
142
143
|
this.status = "processing";
|
|
143
|
-
this.attempts
|
|
144
|
+
this.attempts = toSafeInteger(this.attempts + 1, "Dispatch attempts");
|
|
144
145
|
this.updatedAt = /* @__PURE__ */ new Date();
|
|
145
146
|
}
|
|
146
147
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dispatch.js","names":[],"sources":["../../../src/dispatch/models/Dispatch.ts"],"sourcesContent":["/**\n * Dispatch Model - Persistent dispatch message for inter-agent communication\n *\n * Dispatches are stored in the _smrt_dispatch system table and represent\n * messages sent between agents. They support async processing with retry logic.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { makeId } from '@happyvertical/utils';\nimport type { DispatchMetadata, DispatchStatus } from '../types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Safely parses a JSON column from a `_smrt_dispatch` row.\n *\n * A single corrupted `payload`/`metadata` value must not throw out of the\n * `Dispatch` constructor, since `process()`/`list()` map `fromRow` over a whole\n * batch — one poison row would otherwise stall every dispatch in the batch\n * (#1378). On a parse failure, logs the offending row id + field and falls back\n * to an empty object so the rest of the batch keeps flowing.\n */\nfunction parseJsonField(\n raw: string,\n field: 'payload' | 'metadata',\n rowId: string,\n): Record<string, unknown> {\n try {\n // Only malformed JSON is treated as a poison value; valid JSON parses as\n // before (preserving prior behavior for the well-formed path).\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (error) {\n logger.warn('Skipping corrupted _smrt_dispatch JSON field', {\n dispatchId: rowId,\n field,\n error: error instanceof Error ? error.message : String(error),\n });\n return {};\n }\n}\n\n/**\n * Raw dispatch data as stored in the database\n */\nexport interface DispatchData {\n id: string;\n type: string;\n source: string;\n source_id: string | null;\n payload: string | null;\n status: DispatchStatus;\n attempts: number;\n last_error: string | null;\n processed_at: string | null;\n processed_by: string | null;\n target_subscriber: string | null;\n correlation_id: string | null;\n tenant_id: string | null;\n metadata: string | null;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * Dispatch model for inter-agent communication\n */\nexport class Dispatch {\n /** Table name for this model */\n static readonly tableName = '_smrt_dispatch';\n\n /** Unique identifier */\n id: string;\n\n /** Signal type (e.g., 'campaign.completed') */\n type: string;\n\n /** Emitting agent name */\n source: string;\n\n /** Emitting agent instance ID */\n sourceId: string;\n\n /** Dispatch payload */\n payload: Record<string, unknown>;\n\n /** Current status */\n status: DispatchStatus;\n\n /** Number of processing attempts */\n attempts: number;\n\n /** Last error message if failed */\n lastError: string;\n\n /** When the dispatch was processed */\n processedAt: Date | null;\n\n /** Which subscriber processed it */\n processedBy: string;\n\n /** Target subscriber for fan-out delivery (null for compete mode) */\n targetSubscriber: string | null;\n\n /** Correlation ID for linking request/response dispatch pairs */\n correlationId: string;\n\n /**\n * Tenant the dispatch was emitted in (server-derived, untrusted-input-proof).\n *\n * Stamped from the active tenant context at emit time — never from caller\n * options — so it cannot be spoofed. `null` for global / non-tenant\n * dispatches. Used to isolate reads/claims across tenants (S5 #1398).\n */\n tenantId: string | null;\n\n /** Additional metadata */\n metadata: Record<string, unknown>;\n\n /** Creation timestamp */\n createdAt: Date;\n\n /** Last update timestamp */\n updatedAt: Date;\n\n constructor(data: Partial<DispatchData> = {}) {\n this.id = data.id || makeId();\n this.type = data.type || '';\n this.source = data.source || '';\n this.sourceId = data.source_id || '';\n this.status = (data.status as DispatchStatus) || 'pending';\n this.attempts = data.attempts
|
|
1
|
+
{"version":3,"file":"Dispatch.js","names":[],"sources":["../../../src/dispatch/models/Dispatch.ts"],"sourcesContent":["/**\n * Dispatch Model - Persistent dispatch message for inter-agent communication\n *\n * Dispatches are stored in the _smrt_dispatch system table and represent\n * messages sent between agents. They support async processing with retry logic.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { makeId } from '@happyvertical/utils';\nimport { toSafeInteger } from '../../utils/safe-integer.js';\nimport type { DispatchMetadata, DispatchStatus } from '../types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Safely parses a JSON column from a `_smrt_dispatch` row.\n *\n * A single corrupted `payload`/`metadata` value must not throw out of the\n * `Dispatch` constructor, since `process()`/`list()` map `fromRow` over a whole\n * batch — one poison row would otherwise stall every dispatch in the batch\n * (#1378). On a parse failure, logs the offending row id + field and falls back\n * to an empty object so the rest of the batch keeps flowing.\n */\nfunction parseJsonField(\n raw: string,\n field: 'payload' | 'metadata',\n rowId: string,\n): Record<string, unknown> {\n try {\n // Only malformed JSON is treated as a poison value; valid JSON parses as\n // before (preserving prior behavior for the well-formed path).\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (error) {\n logger.warn('Skipping corrupted _smrt_dispatch JSON field', {\n dispatchId: rowId,\n field,\n error: error instanceof Error ? error.message : String(error),\n });\n return {};\n }\n}\n\n/**\n * Raw dispatch data as stored in the database\n */\nexport interface DispatchData {\n id: string;\n type: string;\n source: string;\n source_id: string | null;\n payload: string | null;\n status: DispatchStatus;\n attempts: number | string | bigint;\n last_error: string | null;\n processed_at: string | null;\n processed_by: string | null;\n target_subscriber: string | null;\n correlation_id: string | null;\n tenant_id: string | null;\n metadata: string | null;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * Dispatch model for inter-agent communication\n */\nexport class Dispatch {\n /** Table name for this model */\n static readonly tableName = '_smrt_dispatch';\n\n /** Unique identifier */\n id: string;\n\n /** Signal type (e.g., 'campaign.completed') */\n type: string;\n\n /** Emitting agent name */\n source: string;\n\n /** Emitting agent instance ID */\n sourceId: string;\n\n /** Dispatch payload */\n payload: Record<string, unknown>;\n\n /** Current status */\n status: DispatchStatus;\n\n /** Number of processing attempts */\n attempts: number;\n\n /** Last error message if failed */\n lastError: string;\n\n /** When the dispatch was processed */\n processedAt: Date | null;\n\n /** Which subscriber processed it */\n processedBy: string;\n\n /** Target subscriber for fan-out delivery (null for compete mode) */\n targetSubscriber: string | null;\n\n /** Correlation ID for linking request/response dispatch pairs */\n correlationId: string;\n\n /**\n * Tenant the dispatch was emitted in (server-derived, untrusted-input-proof).\n *\n * Stamped from the active tenant context at emit time — never from caller\n * options — so it cannot be spoofed. `null` for global / non-tenant\n * dispatches. Used to isolate reads/claims across tenants (S5 #1398).\n */\n tenantId: string | null;\n\n /** Additional metadata */\n metadata: Record<string, unknown>;\n\n /** Creation timestamp */\n createdAt: Date;\n\n /** Last update timestamp */\n updatedAt: Date;\n\n constructor(data: Partial<DispatchData> = {}) {\n this.id = data.id || makeId();\n this.type = data.type || '';\n this.source = data.source || '';\n this.sourceId = data.source_id || '';\n this.status = (data.status as DispatchStatus) || 'pending';\n this.attempts = toSafeInteger(data.attempts ?? 0, 'Dispatch attempts');\n this.lastError = data.last_error || '';\n this.processedBy = data.processed_by || '';\n this.targetSubscriber = data.target_subscriber || null;\n this.correlationId = data.correlation_id || '';\n this.tenantId = data.tenant_id ?? null;\n this.createdAt = data.created_at ? new Date(data.created_at) : new Date();\n this.updatedAt = data.updated_at ? new Date(data.updated_at) : new Date();\n this.processedAt = data.processed_at ? new Date(data.processed_at) : null;\n\n // Parse JSON fields. Guarded so a single corrupted row cannot stall an\n // entire process()/list() batch (#1378).\n this.payload = data.payload\n ? parseJsonField(data.payload, 'payload', this.id)\n : {};\n this.metadata = data.metadata\n ? parseJsonField(data.metadata, 'metadata', this.id)\n : {};\n }\n\n /**\n * Create a Dispatch from a database row\n */\n static fromRow(row: DispatchData): Dispatch {\n return new Dispatch(row);\n }\n\n /**\n * Convert to database row format\n */\n toRow(): DispatchData {\n return {\n id: this.id,\n type: this.type,\n source: this.source,\n source_id: this.sourceId || null,\n payload: JSON.stringify(this.payload),\n status: this.status,\n attempts: this.attempts,\n last_error: this.lastError || null,\n processed_at: this.processedAt?.toISOString() || null,\n processed_by: this.processedBy || null,\n target_subscriber: this.targetSubscriber || null,\n correlation_id: this.correlationId || null,\n tenant_id: this.tenantId ?? null,\n metadata: JSON.stringify(this.metadata),\n created_at: this.createdAt.toISOString(),\n updated_at: new Date().toISOString(),\n };\n }\n\n /**\n * Get dispatch metadata for handlers\n */\n getMetadata(): DispatchMetadata {\n return {\n id: this.id,\n type: this.type,\n source: this.source,\n sourceId: this.sourceId,\n correlationId: this.correlationId,\n createdAt: this.createdAt,\n attempts: this.attempts,\n metadata: this.metadata,\n };\n }\n\n /**\n * Mark as processing\n */\n markProcessing(): void {\n this.status = 'processing';\n this.attempts = toSafeInteger(this.attempts + 1, 'Dispatch attempts');\n this.updatedAt = new Date();\n }\n\n /**\n * Mark as completed\n */\n markCompleted(processedBy: string): void {\n this.status = 'completed';\n this.processedAt = new Date();\n this.processedBy = processedBy;\n this.lastError = '';\n this.updatedAt = new Date();\n }\n\n /**\n * Mark as failed\n */\n markFailed(error: string): void {\n this.status = 'failed';\n this.lastError = error;\n this.updatedAt = new Date();\n }\n\n /**\n * Reset to pending for retry\n */\n resetForRetry(): void {\n this.status = 'pending';\n this.updatedAt = new Date();\n }\n\n /**\n * Check if this dispatch can be retried\n */\n canRetry(maxAttempts: number): boolean {\n return this.status === 'failed' && this.attempts < maxAttempts;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DispatchSubscription.d.ts","sourceRoot":"","sources":["../../../src/dispatch/models/DispatchSubscription.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"DispatchSubscription.d.ts","sourceRoot":"","sources":["../../../src/dispatch/models/DispatchSubscription.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,oBAAoB;IAC/B,gCAAgC;IAChC,MAAM,CAAC,QAAQ,CAAC,SAAS,kCAAkC;IAE3D,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IAEX,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IAEnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IAEnB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAEhB,kFAAkF;IAClF,QAAQ,EAAE,SAAS,GAAG,QAAQ,CAAC;IAE/B,qCAAqC;IACrC,OAAO,EAAE,OAAO,CAAC;IAEjB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB,yBAAyB;IACzB,SAAS,EAAE,IAAI,CAAC;IAEhB,4BAA4B;IAC5B,SAAS,EAAE,IAAI,CAAC;gBAEJ,IAAI,GAAE,OAAO,CAAC,wBAAwB,CAAM;IAkBxD;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,wBAAwB,GAAG,oBAAoB;IAInE;;OAEG;IACH,KAAK,IAAI,wBAAwB;IAcjC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAmCpC;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,MAAM,IAAI,IAAI;IAKd;;OAEG;IACH,OAAO,IAAI,IAAI;CAIhB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toSafeBooleanInteger } from "../../utils/safe-integer.js";
|
|
1
2
|
import { makeId } from "@happyvertical/utils";
|
|
2
3
|
//#region src/dispatch/models/DispatchSubscription.ts
|
|
3
4
|
/**
|
|
@@ -40,7 +41,7 @@ var DispatchSubscription = class DispatchSubscription {
|
|
|
40
41
|
this.subscriber = data.subscriber || "";
|
|
41
42
|
this.handler = data.handler || "handleDispatch";
|
|
42
43
|
this.delivery = data.delivery || "compete";
|
|
43
|
-
this.enabled = data.enabled
|
|
44
|
+
this.enabled = data.enabled != null ? toSafeBooleanInteger(data.enabled, "Dispatch subscription enabled flag") : true;
|
|
44
45
|
this.tenantId = data.tenant_id ?? null;
|
|
45
46
|
this.createdAt = data.created_at ? new Date(data.created_at) : /* @__PURE__ */ new Date();
|
|
46
47
|
this.updatedAt = data.updated_at ? new Date(data.updated_at) : /* @__PURE__ */ new Date();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DispatchSubscription.js","names":[],"sources":["../../../src/dispatch/models/DispatchSubscription.ts"],"sourcesContent":["/**\n * DispatchSubscription Model - Persistent subscription for dispatch processing\n *\n * Subscriptions are stored in _smrt_dispatch_subscriptions and define which\n * agents should receive which dispatch types. Supports wildcard patterns.\n */\n\nimport { makeId } from '@happyvertical/utils';\n\n/**\n * Raw subscription data as stored in the database\n */\nexport interface DispatchSubscriptionData {\n id: string;\n signal_type: string;\n subscriber: string;\n handler: string;\n delivery: string;\n enabled: number;\n tenant_id: string | null;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * DispatchSubscription model for persistent dispatch routing\n */\nexport class DispatchSubscription {\n /** Table name for this model */\n static readonly tableName = '_smrt_dispatch_subscriptions';\n\n /** Unique identifier */\n id: string;\n\n /** Signal type pattern (supports wildcards like 'campaign.*') */\n signalType: string;\n\n /** Subscriber name (usually agent name) */\n subscriber: string;\n\n /** Handler method name */\n handler: string;\n\n /** Delivery mode: 'compete' (first-to-claim) or 'fanout' (per-subscriber copy) */\n delivery: 'compete' | 'fanout';\n\n /** Whether subscription is active */\n enabled: boolean;\n\n /**\n * Tenant the subscription was registered in (server-derived). `null` for\n * global / non-tenant subscriptions. Recorded for auditing and per-tenant\n * subscription separation (S5 #1398).\n */\n tenantId: string | null;\n\n /** Creation timestamp */\n createdAt: Date;\n\n /** Last update timestamp */\n updatedAt: Date;\n\n constructor(data: Partial<DispatchSubscriptionData> = {}) {\n this.id = data.id || makeId();\n this.signalType = data.signal_type || '';\n this.subscriber = data.subscriber || '';\n this.handler = data.handler || 'handleDispatch';\n this.delivery = (data.delivery as 'compete' | 'fanout') || 'compete';\n this.enabled
|
|
1
|
+
{"version":3,"file":"DispatchSubscription.js","names":[],"sources":["../../../src/dispatch/models/DispatchSubscription.ts"],"sourcesContent":["/**\n * DispatchSubscription Model - Persistent subscription for dispatch processing\n *\n * Subscriptions are stored in _smrt_dispatch_subscriptions and define which\n * agents should receive which dispatch types. Supports wildcard patterns.\n */\n\nimport { makeId } from '@happyvertical/utils';\nimport { toSafeBooleanInteger } from '../../utils/safe-integer.js';\n\n/**\n * Raw subscription data as stored in the database\n */\nexport interface DispatchSubscriptionData {\n id: string;\n signal_type: string;\n subscriber: string;\n handler: string;\n delivery: string;\n enabled: number | string | bigint | null;\n tenant_id: string | null;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * DispatchSubscription model for persistent dispatch routing\n */\nexport class DispatchSubscription {\n /** Table name for this model */\n static readonly tableName = '_smrt_dispatch_subscriptions';\n\n /** Unique identifier */\n id: string;\n\n /** Signal type pattern (supports wildcards like 'campaign.*') */\n signalType: string;\n\n /** Subscriber name (usually agent name) */\n subscriber: string;\n\n /** Handler method name */\n handler: string;\n\n /** Delivery mode: 'compete' (first-to-claim) or 'fanout' (per-subscriber copy) */\n delivery: 'compete' | 'fanout';\n\n /** Whether subscription is active */\n enabled: boolean;\n\n /**\n * Tenant the subscription was registered in (server-derived). `null` for\n * global / non-tenant subscriptions. Recorded for auditing and per-tenant\n * subscription separation (S5 #1398).\n */\n tenantId: string | null;\n\n /** Creation timestamp */\n createdAt: Date;\n\n /** Last update timestamp */\n updatedAt: Date;\n\n constructor(data: Partial<DispatchSubscriptionData> = {}) {\n this.id = data.id || makeId();\n this.signalType = data.signal_type || '';\n this.subscriber = data.subscriber || '';\n this.handler = data.handler || 'handleDispatch';\n this.delivery = (data.delivery as 'compete' | 'fanout') || 'compete';\n this.enabled =\n data.enabled != null\n ? toSafeBooleanInteger(\n data.enabled,\n 'Dispatch subscription enabled flag',\n )\n : true;\n this.tenantId = data.tenant_id ?? null;\n this.createdAt = data.created_at ? new Date(data.created_at) : new Date();\n this.updatedAt = data.updated_at ? new Date(data.updated_at) : new Date();\n }\n\n /**\n * Create a DispatchSubscription from a database row\n */\n static fromRow(row: DispatchSubscriptionData): DispatchSubscription {\n return new DispatchSubscription(row);\n }\n\n /**\n * Convert to database row format\n */\n toRow(): DispatchSubscriptionData {\n return {\n id: this.id,\n signal_type: this.signalType,\n subscriber: this.subscriber,\n handler: this.handler,\n delivery: this.delivery,\n enabled: this.enabled ? 1 : 0,\n tenant_id: this.tenantId ?? null,\n created_at: this.createdAt.toISOString(),\n updated_at: new Date().toISOString(),\n };\n }\n\n /**\n * Check if this subscription matches a given signal type\n *\n * Supports wildcard patterns:\n * - 'campaign.*' matches 'campaign.started', 'campaign.completed'\n * - '*' matches everything\n * - 'campaign.*.completed' matches 'campaign.summer.completed'\n *\n * @param signalType - The signal type to match against\n * @returns True if this subscription matches the signal type\n */\n matches(signalType: string): boolean {\n if (!this.enabled) {\n return false;\n }\n\n // Exact match\n if (this.signalType === signalType) {\n return true;\n }\n\n // If no wildcards, no match\n if (!this.signalType.includes('*')) {\n return false;\n }\n\n // Convert wildcard pattern to regex\n // First escape special regex characters except * and .\n // Then convert . to \\. and * to [^.]+\n let pattern = this.signalType;\n\n // Escape regex special chars except * and .\n pattern = pattern.replace(/[+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // Escape dots\n pattern = pattern.replace(/\\./g, '\\\\.');\n\n // Convert * to match one segment (anything except .)\n pattern = pattern.replace(/\\*/g, '[^.]+');\n\n // Create regex for full match\n const regex = new RegExp(`^${pattern}$`);\n\n return regex.test(signalType);\n }\n\n /**\n * Check if this is a wildcard subscription\n */\n isWildcard(): boolean {\n return this.signalType.includes('*');\n }\n\n /**\n * Enable this subscription\n */\n enable(): void {\n this.enabled = true;\n this.updatedAt = new Date();\n }\n\n /**\n * Disable this subscription\n */\n disable(): void {\n this.enabled = false;\n this.updatedAt = new Date();\n }\n}\n"],"mappings":";;;;;;;;;;;;AA4BA,IAAa,uBAAb,MAAa,qBAAqB;;CAEhC,OAAgB,YAAY;;CAG5B;;CAGA;;CAGA;;CAGA;;CAGA;;CAGA;;;;;;CAOA;;CAGA;;CAGA;CAEA,YAAY,OAA0C,CAAC,GAAG;EACxD,KAAK,KAAK,KAAK,MAAM,OAAO;EAC5B,KAAK,aAAa,KAAK,eAAe;EACtC,KAAK,aAAa,KAAK,cAAc;EACrC,KAAK,UAAU,KAAK,WAAW;EAC/B,KAAK,WAAY,KAAK,YAAqC;EAC3D,KAAK,UACH,KAAK,WAAW,OACZ,qBACE,KAAK,SACL,oCACF,IACA;EACN,KAAK,WAAW,KAAK,aAAa;EAClC,KAAK,YAAY,KAAK,aAAa,IAAI,KAAK,KAAK,UAAU,oBAAI,IAAI,KAAK;EACxE,KAAK,YAAY,KAAK,aAAa,IAAI,KAAK,KAAK,UAAU,oBAAI,IAAI,KAAK;CAC1E;;;;CAKA,OAAO,QAAQ,KAAqD;EAClE,OAAO,IAAI,qBAAqB,GAAG;CACrC;;;;CAKA,QAAkC;EAChC,OAAO;GACL,IAAI,KAAK;GACT,aAAa,KAAK;GAClB,YAAY,KAAK;GACjB,SAAS,KAAK;GACd,UAAU,KAAK;GACf,SAAS,KAAK,UAAU,IAAI;GAC5B,WAAW,KAAK,YAAY;GAC5B,YAAY,KAAK,UAAU,YAAY;GACvC,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY;EACrC;CACF;;;;;;;;;;;;CAaA,QAAQ,YAA6B;EACnC,IAAI,CAAC,KAAK,SACR,OAAO;EAIT,IAAI,KAAK,eAAe,YACtB,OAAO;EAIT,IAAI,CAAC,KAAK,WAAW,SAAS,GAAG,GAC/B,OAAO;EAMT,IAAI,UAAU,KAAK;EAGnB,UAAU,QAAQ,QAAQ,qBAAqB,MAAM;EAGrD,UAAU,QAAQ,QAAQ,OAAO,KAAK;EAGtC,UAAU,QAAQ,QAAQ,OAAO,OAAO;EAKxC,OAAO,IAFW,OAAO,IAAI,QAAQ,EAE9B,CAAA,CAAM,KAAK,UAAU;CAC9B;;;;CAKA,aAAsB;EACpB,OAAO,KAAK,WAAW,SAAS,GAAG;CACrC;;;;CAKA,SAAe;EACb,KAAK,UAAU;EACf,KAAK,4BAAY,IAAI,KAAK;CAC5B;;;;CAKA,UAAgB;EACd,KAAK,UAAU;EACf,KAAK,4BAAY,IAAI,KAAK;CAC5B;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/embeddings/storage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/embeddings/storage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAIhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,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;;;;;;;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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chunkArray } from "../utils/chunk.js";
|
|
2
|
+
import { toSafeInteger } from "../utils/safe-integer.js";
|
|
2
3
|
import { CosineSimilarity } from "./similarity.js";
|
|
3
4
|
import { createLogger } from "@happyvertical/logger";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -251,7 +252,7 @@ var EmbeddingStorage = class EmbeddingStorage {
|
|
|
251
252
|
content_hash: row.content_hash,
|
|
252
253
|
embedding: JSON.parse(row.embedding),
|
|
253
254
|
model: row.model,
|
|
254
|
-
dimensions: row.dimensions,
|
|
255
|
+
dimensions: toSafeInteger(row.dimensions, "Embedding dimensions"),
|
|
255
256
|
provider: row.provider,
|
|
256
257
|
created_at: new Date(row.created_at),
|
|
257
258
|
updated_at: new Date(row.updated_at)
|
|
@@ -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 { 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 * 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: row.dimensions as number,\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 { 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 * 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":";;;;;;;;;;;;AAeA,IAAM,SAAS,aAAa,EAAE,OAAO,OAAO,CAAC;;AAG7C,IAAM,gBAAgB;;;;AAKtB,IAAa,mBAAb,MAAa,iBAAiB;;;;;;;;CAQ5B,aAAa,OACX,IACA,MAUA,QACe;EACf,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,KAAK,WAAW;EAEtB,MAAM,GAAG,OACP,oBACA;GAAC;GAAgB;GAAa;GAAc;EAAO,GACnD;GACE;GACA,cAAc,KAAK;GACnB,WAAW,KAAK;GAChB,YAAY,KAAK;GACjB,cAAc,KAAK;GACnB,WAAW,KAAK,UAAU,KAAK,SAAS;GACxC,OAAO,KAAK;GACZ,YAAY,KAAK;GACjB,UAAU,KAAK,YAAY;GAC3B,YAAY;GACZ,YAAY;EACd,CACF;EAGA,IAAI,QACF,IAAI;GACF,MAAM,OAAO,aACX,oBACA;IACE,cAAc,KAAK;IACnB,WAAW,KAAK;IAChB,YAAY,KAAK;IACjB,OAAO,KAAK;GACd,GACA,eACA,KAAK,SACP;EACF,SAAS,OAAO;GAEd,OAAO,KACL,+CAA+C,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACtG;EACF;CAEJ;;;;;;;;;;;;;;;CAgBA,aAAa,cACX,IACA,aACA,WACA,UAYI,CAAC,GACL,QAC0D;EAC1D,MAAM,EAAE,OAAO,OAAO,QAAQ,IAAI,gBAAgB,GAAG,cAAc;EAInE,IAAI,cAAc,KAAA,KAAa,UAAU,WAAW,GAClD,OAAO,CAAC;EAIV,IAAI,QACF,IAAI;GAIF,MAAM,aAAuB,CAAC,mBAAmB;GACjD,MAAM,SAAoB,CAAC,WAAW;GAEtC,IAAI,OAAO;IACT,WAAW,KAAK,iBAAiB,OAAO,SAAS,GAAG;IACpD,OAAO,KAAK,KAAK;GACnB;GAEA,IAAI,OAAO;IACT,WAAW,KAAK,YAAY,OAAO,SAAS,GAAG;IAC/C,OAAO,KAAK,KAAK;GACnB;GAMA,MAAM,WAAW,YACb,WAAW,WAAA,GAA6B,IACxC,CAAC,KAAA,CAAS;GAEd,MAAM,SAA0D,CAAC;GACjE,KAAK,MAAM,SAAS,UAAU;IAC5B,MAAM,kBAAkB,CAAC,GAAG,UAAU;IACtC,MAAM,cAAc,CAAC,GAAG,MAAM;IAC9B,IAAI,OAAO;KACT,MAAM,eAAe,MAAM,KACxB,GAAG,UAAU,IAAI,YAAY,SAAS,IAAI,OAC7C;KACA,gBAAgB,KAAK,iBAAiB,aAAa,KAAK,IAAI,EAAE,EAAE;KAChE,YAAY,KAAK,GAAG,KAAK;IAC3B;IAEA,MAAM,UAAU,MAAM,OAAO,OAC3B,oBACA,eACA,WACA;KACE;KACA,QAAQ;KACR,OAAO,gBAAgB,KAAK,OAAO;KACnC,QAAQ;IACV,CACF;IAIA,OAAO,KACL,GAAG,QAAQ,KAAK,OAAO;KACrB,UAAU,EAAE;KACZ,YAAY,IAAI,EAAE;IACpB,EAAE,CACJ;GACF;GAEA,OAAO,OACJ,QAAQ,MAAM,EAAE,cAAc,aAAa,CAAC,CAC5C,MAAM,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU,CAAC,CAC3C,MAAM,GAAG,KAAK;EACnB,SAAS,OAAO;GAEd,OAAO,KACL,wEAAwE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAC/H;EACF;EAIF,MAAM,mBAAmB,MAAM,iBAAiB,aAC9C,IACA,aACA,OACA,KACF;EAIA,MAAM,aAAa,YAAY,IAAI,IAAI,SAAS,IAAI,KAAA;EACpD,MAAM,aAAa,aACf,iBAAiB,QAAQ,WAAW,WAAW,IAAI,OAAO,SAAS,CAAC,IACpE;EAEJ,IAAI,WAAW,WAAW,GACxB,OAAO,CAAC;EAGV,OAAO,WACJ,KAAK,YAAY;GAChB,UAAU,OAAO;GACjB,YAAY,iBAAiB,UAAU,WAAW,OAAO,SAAS;EACpE,EAAE,CAAC,CACF,QAAQ,SAAS,KAAK,cAAc,aAAa,CAAC,CAClD,MAAM,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU,CAAC,CAC3C,MAAM,GAAG,KAAK;CACnB;;;;;;;;;CAUA,aAAa,oBACX,KACA,YACA,QACe;EACf,MAAM,OAAO,aAAa,oBAAoB,eAAe,UAAU;EACvE,MAAM,OAAO,YAAY,oBAAoB,eAAe;GAC1D;GACA,QAAQ;GACR,MAAM;EACR,CAAC;CACH;;;;;;;;;;;CAYA,aAAa,IACX,IACA,aACA,UACA,WACA,OACiC;EACjC,MAAM,SAAS,MAAM,GAAG,MAAM;;6BAEL,YAAY;0BACf,SAAS;2BACR,UAAU;sBACf,MAAM;;EAGxB,IAAI,CAAC,QAAQ,OAAO;EAEpB,OAAO,iBAAiB,SAAS,MAAM;CACzC;;;;;;;;;CAUA,aAAa,aACX,IACA,aACA,UAC4B;EAC5B,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB,2EACA,aACA,QACF;EAEA,OAAO,KAAK,KAAK,QACf,iBAAiB,SAAS,GAAG,CAC/B;CACF;;;;;;;;;;CAWA,aAAa,aACX,IACA,aACA,WACA,OAC4B;EAC5B,IAAI,MAAM;EACV,MAAM,SAAoB,CAAC,WAAW;EAEtC,IAAI,WAAW;GACb,OAAO;GACP,OAAO,KAAK,SAAS;EACvB;EAEA,IAAI,OAAO;GACT,OAAO;GACP,OAAO,KAAK,KAAK;EACnB;EAEA,MAAM,EAAE,SAAS,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM;EAE9C,OAAO,KAAK,KAAK,QACf,iBAAiB,SAAS,GAAG,CAC/B;CACF;;;;;;;;;CAUA,aAAa,2BACX,IACA,aACA,WACmB;EACnB,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB;mDAEA,aACA,SACF;EAEA,OAAO,KAAK,KAAK,QAAiC,IAAI,SAAmB;CAC3E;;;;;;;;CASA,aAAa,gBACX,IACA,aACA,UACe;EACf,MAAM,GAAG,MACP,yEACA,aACA,QACF;CACF;;;;;;;;;;CAWA,aAAa,OACX,IACA,aACA,UACA,WACA,OACe;EACf,MAAM,GAAG,MACP;mFAEA,aACA,UACA,WACA,KACF;CACF;;;;;;;CAQA,aAAa,YAAY,IAAyC;EAChE,IAAI;GACF,MAAM,GAAG,MAAM,wCAAwC;GACvD,OAAO;EACT,QAAQ;GACN,OAAO;EACT;CACF;;;;CAKA,OAAe,SAAS,KAA+C;EACrE,OAAO;GACL,IAAI,IAAI;GACR,cAAc,IAAI;GAClB,WAAW,IAAI;GACf,YAAY,IAAI;GAChB,cAAc,IAAI;GAClB,WAAW,KAAK,MAAM,IAAI,SAAmB;GAC7C,OAAO,IAAI;GACX,YAAY,cAAc,IAAI,YAAY,sBAAsB;GAChE,UAAU,IAAI;GACd,YAAY,IAAI,KAAK,IAAI,UAAoB;GAC7C,YAAY,IAAI,KAAK,IAAI,UAAoB;EAC/C;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/learning/memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/learning/memory.ts"],"names":[],"mappings":"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"}
|
package/dist/learning/memory.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toSafeInteger } from "../utils/safe-integer.js";
|
|
1
2
|
//#region src/learning/memory.ts
|
|
2
3
|
/** Default reinforcement thresholds. */
|
|
3
4
|
var DEFAULT_LEARNING_CONFIG = {
|
|
@@ -161,8 +162,8 @@ var LearningMemory = class {
|
|
|
161
162
|
if (existing) {
|
|
162
163
|
const currentConfidence = Number(existing.confidence ?? 1);
|
|
163
164
|
const nextConfidence = this.nextConfidence(currentConfidence, success);
|
|
164
|
-
const successCount =
|
|
165
|
-
const failureCount =
|
|
165
|
+
const successCount = toSafeInteger(toSafeInteger(existing.success_count ?? 0, "Learning memory success count") + (success ? 1 : 0), "Learning memory success count");
|
|
166
|
+
const failureCount = toSafeInteger(toSafeInteger(existing.failure_count ?? 0, "Learning memory failure count") + (success ? 0 : 1), "Learning memory failure count");
|
|
166
167
|
const data = {
|
|
167
168
|
confidence: nextConfidence,
|
|
168
169
|
success_count: successCount,
|
|
@@ -275,8 +276,8 @@ var LearningMemory = class {
|
|
|
275
276
|
key: String(row.key),
|
|
276
277
|
value: parseValue(row.value),
|
|
277
278
|
confidence: effective,
|
|
278
|
-
successCount:
|
|
279
|
-
failureCount:
|
|
279
|
+
successCount: toSafeInteger(row.success_count ?? 0, "Learning memory success count"),
|
|
280
|
+
failureCount: toSafeInteger(row.failure_count ?? 0, "Learning memory failure count"),
|
|
280
281
|
lastUsedAt,
|
|
281
282
|
source: "context"
|
|
282
283
|
});
|