@fgv/ts-agent-memory-sqlite-vec 5.1.0-49 → 5.1.0-50

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.
@@ -1 +1 @@
1
- {"version":3,"file":"sqliteVecVectorIndex.js","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/sqliteVecVectorIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport type BetterSqlite3 from 'better-sqlite3';\nimport { load as loadSqliteVec } from 'sqlite-vec';\nimport { Result, captureAsyncResult, captureResult, fail, succeed } from '@fgv/ts-utils';\nimport {\n IEdgeTarget,\n IMemoryRecordSource,\n IScopedMemoryRecord,\n ISkippedVectorRecord,\n IVectorIndex,\n IVectorQueryHit,\n IVectorRebuildOptions,\n IVectorRebuildReport,\n MemoryEmbedder,\n MemoryId,\n MemoryScopeKey,\n edgeTargetKey\n} from '@fgv/ts-agent-memory';\nimport { ISqliteVecVectorIndexCreateParams } from './model';\n\n/**\n * Invoke a consumer-supplied hook that already returns a `Result`, converting a\n * synchronous throw or a promise rejection into a `Failure` rather than letting\n * it escape. `captureAsyncResult` wraps the hook's own `Result`, so the outcome\n * is flattened back to one level.\n *\n * @remarks\n * This is `@fgv/ts-utils`' own `_invokeDeferred` shape (see `mapResultsAsync`),\n * which is `@internal` there and so cannot be imported. `@fgv/ts-agent-memory`\n * carries an identical private copy for the in-memory index. Exporting a single\n * `AsyncDeferredResult`-invoking primitive from `ts-utils` is the right home and\n * is recorded in `docs/TECH_DEBT.md`; duplicating three lines twice is the\n * cheaper thing to do from inside this stream than widening it to a foundational\n * library.\n */\nasync function invokeHook<T>(hook: () => Promise<Result<T>>): Promise<Result<T>> {\n return (await captureAsyncResult(hook)).onSuccess((inner) => inner);\n}\n\n/**\n * Compose the failure that aborted a rebuild with the outcome of the rollback\n * that followed it. A rollback that ALSO fails is worth saying out loud: the\n * `'fail'` path promises an empty index, and a caller that retries against a\n * table which is neither the old index nor empty is working from a state the\n * contract never described.\n */\nfunction withRollbackNote(error: string, rollback: Result<true>): string {\n return rollback.isFailure() ? `${error} (rollback also failed: ${rollback.message})` : error;\n}\n\n/** Default name for the `vec0` virtual table. */\nconst DEFAULT_TABLE_NAME: string = 'memory_vectors';\n\n/** A simple SQL identifier — the only shape allowed for the table name (it is interpolated into DDL). */\nconst IDENTIFIER_RE: RegExp = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** One KNN row as returned by the `vec0` MATCH query. */\ninterface IKnnRow {\n readonly target_key: string;\n readonly distance: number;\n}\n\n/**\n * A persistent, `sqlite-vec`-backed `IVectorIndex` for `@fgv/ts-agent-memory`.\n *\n * @remarks\n * This is the **durable** counterpart to the in-memory `InMemoryCosineIndex`:\n * embeddings live in a `sqlite-vec` `vec0` virtual table inside a `better-sqlite3`\n * database, so they survive a process restart. A consumer that wires this index\n * into `FileTreeMemoryStore` (instead of the in-memory index) opens an existing\n * vault **without re-embedding it** — the vectors are already on disk. New writes\n * still flow through the store's incremental embed-on-write path; there is no core\n * store change.\n *\n * The index is keyed by the canonical `edgeTargetKey` of each record's\n * scope-qualified `(scope, id)` address (a `TEXT PRIMARY KEY` on the `vec0` table),\n * so two records that share a filename stem across scopes never collide. The\n * dimension is established by the first `add` (the `vec0` column is fixed-width) and\n * recovered from the table schema when a persistent file is reopened; every later\n * `add`/`query` must match it or fail loudly, exactly as the in-memory index does.\n * Similarity is cosine (`distance_metric=cosine`): the returned `score` is\n * `1 - cosineDistance`, i.e. cosine similarity in `[-1, 1]`, higher = more similar —\n * byte-for-byte the same scoring contract as `InMemoryCosineIndex`.\n *\n * Query is a brute-force `vec0` KNN scan (not an ANN structure): correct and\n * durable, appropriate for the same \"thousands of records\" regime the in-memory\n * index targets. Large-N ANN indexing is explicitly out of scope — see the README.\n *\n * The `better-sqlite3` `Database` is consumer-owned (bring-your-own): this index\n * loads the `sqlite-vec` extension onto it and reads/writes the table, but never\n * opens or closes the connection.\n * @public\n */\nexport class SqliteVecVectorIndex implements IVectorIndex {\n private readonly _db: BetterSqlite3.Database;\n private readonly _table: string;\n /** The dimension of every stored vector; `undefined` until the table exists (first `add` or a reopened non-empty file). */\n private _dimension: number | undefined;\n /** Prepared statements; created once the table exists (established or recovered). */\n private _stmts: ISqliteVecStatements | undefined;\n\n private constructor(db: BetterSqlite3.Database, table: string, dimension: number | undefined) {\n this._db = db;\n this._table = table;\n this._dimension = dimension;\n this._stmts = dimension === undefined ? undefined : this._prepare();\n }\n\n /** The number of vectors currently held. Zero before the first `add`. */\n public get size(): number {\n if (this._stmts === undefined) {\n return 0;\n }\n return (this._stmts.count.get() as { c: number }).c;\n }\n\n /**\n * Family-convention factory. Loads the `sqlite-vec` extension onto the supplied\n * `better-sqlite3` connection and, if the vector table already exists (a reopened\n * persistent file), recovers its established dimension so no re-embedding is\n * needed on open.\n *\n * @param params - See {@link ISqliteVecVectorIndexCreateParams}.\n * @returns `Success` with the index, or `Failure` if the table name is not a\n * simple identifier or the extension fails to load.\n */\n public static create(params: ISqliteVecVectorIndexCreateParams): Promise<Result<SqliteVecVectorIndex>> {\n const table: string = params.tableName ?? DEFAULT_TABLE_NAME;\n if (!IDENTIFIER_RE.test(table)) {\n return Promise.resolve(fail(`sqlite-vec index: table name '${table}' is not a simple SQL identifier`));\n }\n return Promise.resolve(\n captureResult(() => {\n loadSqliteVec(params.database);\n const dimension: number | undefined = SqliteVecVectorIndex._readExistingDimension(\n params.database,\n table\n );\n return new SqliteVecVectorIndex(params.database, table, dimension);\n }).withErrorFormat((e) => `sqlite-vec index: failed to initialize: ${e}`)\n );\n }\n\n /** {@inheritDoc IVectorIndex.add} */\n public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {\n const key: string = edgeTargetKey(target);\n if (vector.length === 0) {\n return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));\n }\n if (this._dimension !== undefined && vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n return Promise.resolve(\n captureResult(() => {\n if (this._stmts === undefined) {\n this._createTable(vector.length);\n this._dimension = vector.length;\n this._stmts = this._prepare();\n }\n this._stmts.replace(key, SqliteVecVectorIndex._toBlob(vector));\n return key;\n }).withErrorFormat((e) => `vector index: cannot add '${key}': ${e}`)\n );\n }\n\n /** {@inheritDoc IVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n return Promise.resolve(\n captureResult(() => {\n // Idempotent: removing a target with no embedding (or before any `add`\n // created the table) still succeeds.\n if (this._stmts !== undefined) {\n this._stmts.delete.run(edgeTargetKey(target));\n }\n return target;\n }).withErrorFormat((e) => `vector index: cannot remove '${edgeTargetKey(target)}': ${e}`)\n );\n }\n\n /**\n * Re-embed every record from `source` and rebuild the persisted index — see\n * `IVectorIndex.rebuild` for the mode semantics, which this implementation\n * matches exactly.\n *\n * @remarks\n * **Not atomic, and cannot be.** `better-sqlite3` transactions are synchronous,\n * so one cannot span the `await embed(...)` calls this loop makes — unlike\n * {@link SqliteVecVectorIndex.add}, which wraps its delete-then-insert. The\n * `'fail'` / `'skip'` modes therefore cover only failures JavaScript can catch:\n * a process kill mid-rebuild leaves the table holding neither the old index nor\n * the complete new one, and the remedy is to run `rebuild` again.\n */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: MemoryEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<Result<IVectorRebuildReport>> {\n const lenient: boolean = (options?.onRecordError ?? 'fail') === 'skip';\n // `source` is consumer-supplied, so a throw or rejection becomes a `Failure`\n // here rather than escaping as an exception.\n const listed: Result<ReadonlyArray<IScopedMemoryRecord>> = await invokeHook(() => source.list());\n if (listed.isFailure()) {\n // Deliberately BEFORE any clear: a failed list is no evidence about the\n // vectors already held, and no re-embedding has been attempted, so there is\n // no half-rebuilt state to protect against. Clearing here would destroy a\n // healthy persisted index over a transient read error.\n return fail(`vector index rebuild: failed to list records: ${listed.message}`);\n }\n const cleared: Result<true> = this._clear();\n if (cleared.isFailure()) {\n return fail(`vector index rebuild: failed to clear the index: ${cleared.message}`);\n }\n let declined: number = 0;\n const skipped: ISkippedVectorRecord[] = [];\n for (const scoped of listed.value) {\n // Likewise capture-wrapped: an embedder that throws mid-loop would\n // otherwise escape past the `'fail'` rollback below, leaving this DURABLE\n // table holding a partial index that survives the process.\n const embedded: Result<Float32Array | undefined> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `vector index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n if (!lenient) {\n return fail(withRollbackNote(error, this._clear()));\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n if (embedded.value === undefined) {\n declined++;\n continue;\n }\n const added: Result<string> = await this.add(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `vector index rebuild: ${added.message}`;\n if (!lenient) {\n return fail(withRollbackNote(error, this._clear()));\n }\n skipped.push({ target: scoped.target, error });\n }\n }\n return captureResult(() => this.size)\n .withErrorFormat((msg) => `vector index rebuild: failed to count the rebuilt index: ${msg}`)\n .onSuccess((indexed) => succeed({ indexed, declined, skipped }));\n }\n\n /**\n * Empty the table. Deliberately does NOT drop it or forget the established\n * dimension: the `vec0` table's dimension is fixed at creation and a re-embed at\n * a different dimension needs a drop-and-re-index, which is a consumer decision\n * (see the package README on `vec0` schema changes), not something a rebuild\n * should do silently.\n */\n private _clear(): Result<true> {\n if (this._stmts === undefined) {\n return succeed(true);\n }\n // Capture-wrapped like `add` / `remove` / `query`: a closed connection or an\n // I/O error here is a `Failure`, not an exception thrown out of a method\n // whose signature promises a `Result`.\n return captureResult(() => this._db.prepare(`DELETE FROM \"${this._table}\"`).run()).onSuccess(() =>\n succeed(true)\n );\n }\n\n /** {@inheritDoc IVectorIndex.query} */\n public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._stmts === undefined) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n return Promise.resolve(\n captureResult<ReadonlyArray<IVectorQueryHit>>(() => {\n const rows: ReadonlyArray<IKnnRow> = this._stmts!.query.all(\n SqliteVecVectorIndex._toBlob(vector),\n topK\n ) as ReadonlyArray<IKnnRow>;\n // sqlite-vec returns rows in ascending distance (nearest first); score is\n // `1 - cosineDistance` = cosine similarity, so descending score is preserved.\n return rows.map((row) => ({\n target: SqliteVecVectorIndex._parseKey(row.target_key),\n score: 1 - row.distance\n }));\n }).withErrorFormat((e) => `vector index: query failed: ${e}`)\n );\n }\n\n /** Create the `vec0` virtual table with the established dimension. */\n private _createTable(dimension: number): void {\n this._db.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${this._table}\" USING vec0(` +\n `target_key TEXT PRIMARY KEY, embedding float[${dimension}] distance_metric=cosine)`\n );\n }\n\n /** Prepare the statements the index reuses. Requires the table to exist. */\n private _prepare(): ISqliteVecStatements {\n const del: BetterSqlite3.Statement = this._db.prepare(\n `DELETE FROM \"${this._table}\" WHERE target_key = ?`\n );\n const ins: BetterSqlite3.Statement = this._db.prepare(\n `INSERT INTO \"${this._table}\"(target_key, embedding) VALUES (?, ?)`\n );\n // vec0 rejects INSERT OR REPLACE on a TEXT primary key, so replace is a\n // delete-then-insert inside a single transaction.\n const replaceTxn: BetterSqlite3.Transaction<(key: string, blob: Uint8Array) => void> =\n this._db.transaction((key: string, blob: Uint8Array) => {\n del.run(key);\n ins.run(key, blob);\n });\n return {\n delete: del,\n replace: (key: string, blob: Uint8Array): void => {\n replaceTxn(key, blob);\n },\n query: this._db.prepare(\n `SELECT target_key, distance FROM \"${this._table}\" WHERE embedding MATCH ? AND k = ?`\n ),\n count: this._db.prepare(`SELECT count(*) AS c FROM \"${this._table}\"`)\n };\n }\n\n /**\n * Recover the established dimension of an existing `vec0` table from its stored\n * `CREATE VIRTUAL TABLE` SQL (`float[<n>]`). Returns `undefined` when the table\n * does not exist yet (a fresh database — dimension is set by the first `add`).\n */\n private static _readExistingDimension(db: BetterSqlite3.Database, table: string): number | undefined {\n const row: { sql: string } | undefined = db\n .prepare(\"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?\")\n .get(table) as { sql: string } | undefined;\n if (row === undefined) {\n return undefined;\n }\n const match: RegExpMatchArray | null = row.sql.match(/float\\[(\\d+)\\]/);\n return match === null ? undefined : Number(match[1]);\n }\n\n /** Pack a `Float32Array` as the little-endian byte blob `vec0` stores. Copies, so the caller may reuse its buffer. */\n private static _toBlob(vector: Float32Array): Uint8Array {\n return new Uint8Array(Float32Array.from(vector).buffer);\n }\n\n /**\n * Reverse `edgeTargetKey` — the canonical key is `scope\\0id` with NUL\n * excluded from both components, so the first NUL splits it unambiguously.\n */\n private static _parseKey(key: string): IEdgeTarget {\n const nul: number = key.indexOf('\\0');\n return {\n scope: key.slice(0, nul) as unknown as MemoryScopeKey,\n id: key.slice(nul + 1) as unknown as MemoryId\n };\n }\n}\n\n/** The prepared statements / helpers the index reuses once its table exists. */\ninterface ISqliteVecStatements {\n readonly delete: BetterSqlite3.Statement;\n readonly replace: (key: string, blob: Uint8Array) => void;\n readonly query: BetterSqlite3.Statement;\n readonly count: BetterSqlite3.Statement;\n}\n"]}
1
+ {"version":3,"file":"sqliteVecVectorIndex.js","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/sqliteVecVectorIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport type BetterSqlite3 from 'better-sqlite3';\nimport { load as loadSqliteVec } from 'sqlite-vec';\nimport {\n DetailedResult,\n Result,\n captureResult,\n fail,\n failWithDetail,\n succeed,\n succeedWithDetail\n} from '@fgv/ts-utils';\nimport {\n IEdgeTarget,\n IMemoryRecordListing,\n IMemoryRecordSource,\n ISkippedVectorRecord,\n IVectorIndex,\n IVectorQueryHit,\n IVectorRebuildOptions,\n IVectorRebuildReport,\n Kind,\n MemoryEmbedder,\n MemoryId,\n MemoryScopeKey,\n edgeTargetKey\n} from '@fgv/ts-agent-memory';\nimport { invokeHook, tally, withRollbackNote } from './rebuildHelpers';\nimport { ISqliteVecVectorIndexCreateParams } from './model';\n\n/** Default name for the `vec0` virtual table. */\nconst DEFAULT_TABLE_NAME: string = 'memory_vectors';\n\n/** A simple SQL identifier — the only shape allowed for the table name (it is interpolated into DDL). */\nconst IDENTIFIER_RE: RegExp = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** One KNN row as returned by the `vec0` MATCH query. */\ninterface IKnnRow {\n readonly target_key: string;\n readonly distance: number;\n}\n\n/**\n * A persistent, `sqlite-vec`-backed `IVectorIndex` for `@fgv/ts-agent-memory`.\n *\n * @remarks\n * This is the **durable** counterpart to the in-memory `InMemoryCosineIndex`:\n * embeddings live in a `sqlite-vec` `vec0` virtual table inside a `better-sqlite3`\n * database, so they survive a process restart. A consumer that wires this index\n * into `FileTreeMemoryStore` (instead of the in-memory index) opens an existing\n * vault **without re-embedding it** — the vectors are already on disk. New writes\n * still flow through the store's incremental embed-on-write path; there is no core\n * store change.\n *\n * The index is keyed by the canonical `edgeTargetKey` of each record's\n * scope-qualified `(scope, id)` address (a `TEXT PRIMARY KEY` on the `vec0` table),\n * so two records that share a filename stem across scopes never collide. The\n * dimension is established by the first `add` (the `vec0` column is fixed-width) and\n * recovered from the table schema when a persistent file is reopened; every later\n * `add`/`query` must match it or fail loudly, exactly as the in-memory index does.\n * Similarity is cosine (`distance_metric=cosine`): the returned `score` is\n * `1 - cosineDistance`, i.e. cosine similarity in `[-1, 1]`, higher = more similar —\n * byte-for-byte the same scoring contract as `InMemoryCosineIndex`.\n *\n * Query is a brute-force `vec0` KNN scan (not an ANN structure): correct and\n * durable, appropriate for the same \"thousands of records\" regime the in-memory\n * index targets. Large-N ANN indexing is explicitly out of scope — see the README.\n *\n * The `better-sqlite3` `Database` is consumer-owned (bring-your-own): this index\n * loads the `sqlite-vec` extension onto it and reads/writes the table, but never\n * opens or closes the connection.\n * @public\n */\nexport class SqliteVecVectorIndex implements IVectorIndex {\n private readonly _db: BetterSqlite3.Database;\n private readonly _table: string;\n /** The dimension of every stored vector; `undefined` until the table exists (first `add` or a reopened non-empty file). */\n private _dimension: number | undefined;\n /** Prepared statements; created once the table exists (established or recovered). */\n private _stmts: ISqliteVecStatements | undefined;\n\n private constructor(db: BetterSqlite3.Database, table: string, dimension: number | undefined) {\n this._db = db;\n this._table = table;\n this._dimension = dimension;\n this._stmts = dimension === undefined ? undefined : this._prepare();\n }\n\n /** The number of vectors currently held. Zero before the first `add`. */\n public get size(): number {\n if (this._stmts === undefined) {\n return 0;\n }\n // `Number(...)` narrows the count in case the consumer enabled better-sqlite3\n // safe-integer mode (`db.defaultSafeIntegers(true)`), which returns `count(*)`\n // as a `bigint`. Without it a `bigint` leaks through a `number`-typed contract\n // member — and now through `IIndexCoverage.indexSize`, which is also declared\n // `number`, so the coverage report would carry a value of the wrong runtime\n // type. `SqliteVecFragmentIndex`'s two counts have always converted; this one\n // was the outlier.\n return Number((this._stmts.count.get() as { c: number | bigint }).c);\n }\n\n /**\n * Family-convention factory. Loads the `sqlite-vec` extension onto the supplied\n * `better-sqlite3` connection and, if the vector table already exists (a reopened\n * persistent file), recovers its established dimension so no re-embedding is\n * needed on open.\n *\n * @param params - See {@link ISqliteVecVectorIndexCreateParams}.\n * @returns `Success` with the index, or `Failure` if the table name is not a\n * simple identifier or the extension fails to load.\n */\n public static create(params: ISqliteVecVectorIndexCreateParams): Promise<Result<SqliteVecVectorIndex>> {\n const table: string = params.tableName ?? DEFAULT_TABLE_NAME;\n if (!IDENTIFIER_RE.test(table)) {\n return Promise.resolve(fail(`sqlite-vec index: table name '${table}' is not a simple SQL identifier`));\n }\n return Promise.resolve(\n captureResult(() => {\n loadSqliteVec(params.database);\n const dimension: number | undefined = SqliteVecVectorIndex._readExistingDimension(\n params.database,\n table\n );\n return new SqliteVecVectorIndex(params.database, table, dimension);\n }).withErrorFormat((e) => `sqlite-vec index: failed to initialize: ${e}`)\n );\n }\n\n /** {@inheritDoc IVectorIndex.add} */\n public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {\n const key: string = edgeTargetKey(target);\n if (vector.length === 0) {\n return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));\n }\n if (this._dimension !== undefined && vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n return Promise.resolve(\n captureResult(() => {\n if (this._stmts === undefined) {\n this._createTable(vector.length);\n this._dimension = vector.length;\n this._stmts = this._prepare();\n }\n this._stmts.replace(key, SqliteVecVectorIndex._toBlob(vector));\n return key;\n }).withErrorFormat((e) => `vector index: cannot add '${key}': ${e}`)\n );\n }\n\n /** {@inheritDoc IVectorIndex.has} */\n public has(target: IEdgeTarget): Promise<Result<boolean>> {\n return Promise.resolve(\n captureResult(() => {\n // Before any add has created the table there is nothing held, which is a\n // truthful `false` rather than an error — same posture as `remove`'s\n // idempotence and `size`'s zero.\n if (this._stmts === undefined) {\n return false;\n }\n return this._stmts.has.get(edgeTargetKey(target)) !== undefined;\n }).withErrorFormat((e) => `vector index: cannot check '${edgeTargetKey(target)}': ${e}`)\n );\n }\n\n /** {@inheritDoc IVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n return Promise.resolve(\n captureResult(() => {\n // Idempotent: removing a target with no embedding (or before any `add`\n // created the table) still succeeds.\n if (this._stmts !== undefined) {\n this._stmts.delete.run(edgeTargetKey(target));\n }\n return target;\n }).withErrorFormat((e) => `vector index: cannot remove '${edgeTargetKey(target)}': ${e}`)\n );\n }\n\n /**\n * Re-embed every record from `source` and rebuild the persisted index — see\n * `IVectorIndex.rebuild` for the mode semantics, which this implementation\n * matches exactly.\n *\n * @remarks\n * **Not atomic, and cannot be.** `better-sqlite3` transactions are synchronous,\n * so one cannot span the `await embed(...)` calls this loop makes — unlike\n * {@link SqliteVecVectorIndex.add}, which wraps its delete-then-insert. The\n * `'fail'` / `'skip'` modes therefore cover only failures JavaScript can catch:\n * a process kill mid-rebuild leaves the table holding neither the old index nor\n * the complete new one, and the remedy is to run `rebuild` again.\n */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: MemoryEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<DetailedResult<IVectorRebuildReport, IVectorRebuildReport>> {\n const lenient: boolean = (options?.onRecordError ?? 'fail') === 'skip';\n // `source` is consumer-supplied, so a throw or rejection becomes a `Failure`\n // here rather than escaping as an exception.\n const listed: Result<IMemoryRecordListing> = await invokeHook(() => source.list());\n if (listed.isFailure()) {\n // Deliberately BEFORE any clear: a failed list is no evidence about the\n // vectors already held, and no re-embedding has been attempted, so there is\n // no half-rebuilt state to protect against. Clearing here would destroy a\n // healthy persisted index over a transient read error. No report either, for\n // the same reason — there is nothing this call disturbed to describe.\n return failWithDetail(`vector index rebuild: failed to list records: ${listed.message}`);\n }\n const cleared: Result<true> = this._clear();\n if (cleared.isFailure()) {\n // Also nothing established: the table still holds whatever it held.\n return failWithDetail(`vector index rebuild: failed to clear the index: ${cleared.message}`);\n }\n const indexed: Map<Kind, number> = new Map<Kind, number>();\n const declined: Map<Kind, number> = new Map<Kind, number>();\n const skipped: ISkippedVectorRecord[] = [];\n // Absent stays absent — only the source knows whether it filtered anything.\n const report = (): IVectorRebuildReport => ({\n indexed,\n declined,\n excluded: listed.value.excluded,\n skipped\n });\n for (const scoped of listed.value.records) {\n const kind: Kind = scoped.record.envelope.kind;\n // Likewise capture-wrapped: an embedder that throws mid-loop would\n // otherwise escape past the `'fail'` rollback below, leaving this DURABLE\n // table holding a partial index that survives the process.\n const embedded: Result<Float32Array | undefined> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `vector index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n if (!lenient) {\n return failWithDetail(withRollbackNote(error, this._clear()), report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n if (embedded.value === undefined) {\n tally(declined, kind);\n continue;\n }\n const added: Result<string> = await this.add(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `vector index rebuild: ${added.message}`;\n if (!lenient) {\n return failWithDetail(withRollbackNote(error, this._clear()), report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // Tallied in the loop rather than read back off `size` at the end. That\n // `COUNT` was also the only fallible step in assembling the report, so the\n // per-kind tally removes a failure path as well as a rounding of the answer.\n tally(indexed, kind);\n }\n return succeedWithDetail(report());\n }\n\n /**\n * **Empties the rows; does NOT release the table's declared dimension.** That\n * is a `vec0` constraint rather than a choice — the dimension is schema, and\n * there is no `ALTER TABLE` for it — so a rebuild at a new dimension fails\n * here where it would succeed on the in-memory sibling, which forgets its\n * dimension on reset. Changing dimension needs a drop-and-re-index; see the\n * note on `IVectorIndex.rebuild`.\n */\n private _clear(): Result<true> {\n if (this._stmts === undefined) {\n return succeed(true);\n }\n // Capture-wrapped like `add` / `remove` / `query`: a closed connection or an\n // I/O error here is a `Failure`, not an exception thrown out of a method\n // whose signature promises a `Result`.\n return captureResult(() => this._db.prepare(`DELETE FROM \"${this._table}\"`).run()).onSuccess(() =>\n succeed(true)\n );\n }\n\n /** {@inheritDoc IVectorIndex.query} */\n public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._stmts === undefined) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n return Promise.resolve(\n captureResult<ReadonlyArray<IVectorQueryHit>>(() => {\n const rows: ReadonlyArray<IKnnRow> = this._stmts!.query.all(\n SqliteVecVectorIndex._toBlob(vector),\n topK\n ) as ReadonlyArray<IKnnRow>;\n // sqlite-vec returns rows in ascending distance (nearest first); score is\n // `1 - cosineDistance` = cosine similarity, so descending score is preserved.\n return rows.map((row) => ({\n target: SqliteVecVectorIndex._parseKey(row.target_key),\n score: 1 - row.distance\n }));\n }).withErrorFormat((e) => `vector index: query failed: ${e}`)\n );\n }\n\n /** Create the `vec0` virtual table with the established dimension. */\n private _createTable(dimension: number): void {\n this._db.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${this._table}\" USING vec0(` +\n `target_key TEXT PRIMARY KEY, embedding float[${dimension}] distance_metric=cosine)`\n );\n }\n\n /** Prepare the statements the index reuses. Requires the table to exist. */\n private _prepare(): ISqliteVecStatements {\n const del: BetterSqlite3.Statement = this._db.prepare(\n `DELETE FROM \"${this._table}\" WHERE target_key = ?`\n );\n const ins: BetterSqlite3.Statement = this._db.prepare(\n `INSERT INTO \"${this._table}\"(target_key, embedding) VALUES (?, ?)`\n );\n // vec0 rejects INSERT OR REPLACE on a TEXT primary key, so replace is a\n // delete-then-insert inside a single transaction.\n const replaceTxn: BetterSqlite3.Transaction<(key: string, blob: Uint8Array) => void> =\n this._db.transaction((key: string, blob: Uint8Array) => {\n del.run(key);\n ins.run(key, blob);\n });\n return {\n delete: del,\n replace: (key: string, blob: Uint8Array): void => {\n replaceTxn(key, blob);\n },\n query: this._db.prepare(\n `SELECT target_key, distance FROM \"${this._table}\" WHERE embedding MATCH ? AND k = ?`\n ),\n count: this._db.prepare(`SELECT count(*) AS c FROM \"${this._table}\"`),\n // `LIMIT 1` rather than a count: membership needs existence, not cardinality,\n // and vec0 can stop at the first row.\n has: this._db.prepare(`SELECT 1 FROM \"${this._table}\" WHERE target_key = ? LIMIT 1`)\n };\n }\n\n /**\n * Recover the established dimension of an existing `vec0` table from its stored\n * `CREATE VIRTUAL TABLE` SQL (`float[<n>]`). Returns `undefined` when the table\n * does not exist yet (a fresh database — dimension is set by the first `add`).\n */\n private static _readExistingDimension(db: BetterSqlite3.Database, table: string): number | undefined {\n const row: { sql: string } | undefined = db\n .prepare(\"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?\")\n .get(table) as { sql: string } | undefined;\n if (row === undefined) {\n return undefined;\n }\n const match: RegExpMatchArray | null = row.sql.match(/float\\[(\\d+)\\]/);\n return match === null ? undefined : Number(match[1]);\n }\n\n /** Pack a `Float32Array` as the little-endian byte blob `vec0` stores. Copies, so the caller may reuse its buffer. */\n private static _toBlob(vector: Float32Array): Uint8Array {\n return new Uint8Array(Float32Array.from(vector).buffer);\n }\n\n /**\n * Reverse `edgeTargetKey` — the canonical key is `scope\\0id` with NUL\n * excluded from both components, so the first NUL splits it unambiguously.\n */\n private static _parseKey(key: string): IEdgeTarget {\n const nul: number = key.indexOf('\\0');\n return {\n scope: key.slice(0, nul) as unknown as MemoryScopeKey,\n id: key.slice(nul + 1) as unknown as MemoryId\n };\n }\n}\n\n/** The prepared statements / helpers the index reuses once its table exists. */\ninterface ISqliteVecStatements {\n readonly delete: BetterSqlite3.Statement;\n readonly replace: (key: string, blob: Uint8Array) => void;\n readonly query: BetterSqlite3.Statement;\n readonly count: BetterSqlite3.Statement;\n readonly has: BetterSqlite3.Statement;\n}\n"]}
@@ -1,7 +1,10 @@
1
1
  import type BetterSqlite3 from 'better-sqlite3';
2
+ import { DetailedResult } from '@fgv/ts-utils';
3
+ import { FragmentEmbedder } from '@fgv/ts-agent-memory';
2
4
  import { IEdgeTarget } from '@fgv/ts-agent-memory';
3
5
  import { IEmbeddedFragment } from '@fgv/ts-agent-memory';
4
6
  import { IFragmentVectorIndex } from '@fgv/ts-agent-memory';
7
+ import { IFragmentVectorRebuildReport } from '@fgv/ts-agent-memory';
5
8
  import { IMemoryRecordSource } from '@fgv/ts-agent-memory';
6
9
  import { IVectorIndex } from '@fgv/ts-agent-memory';
7
10
  import { IVectorQueryHit } from '@fgv/ts-agent-memory';
@@ -132,6 +135,19 @@ export declare class SqliteVecFragmentIndex implements IFragmentVectorIndex {
132
135
  addFragments(target: IEdgeTarget, fragments: ReadonlyArray<IEmbeddedFragment>): Promise<Result<number>>;
133
136
  /** {@inheritDoc IFragmentVectorIndex.remove} */
134
137
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
138
+ /** {@inheritDoc IFragmentVectorIndex.has} */
139
+ has(target: IEdgeTarget): Promise<Result<boolean>>;
140
+ /** {@inheritDoc IFragmentVectorIndex.rebuild} */
141
+ rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>>;
142
+ /**
143
+ * **Empties the rows; does NOT release the table's declared dimension.** That
144
+ * is a `vec0` constraint rather than a choice — the dimension is schema, and
145
+ * there is no `ALTER TABLE` for it — so a rebuild at a new dimension fails
146
+ * here where it would succeed on the in-memory sibling, which forgets its
147
+ * dimension on reset. Changing dimension needs a drop-and-re-index; see the
148
+ * note on `IVectorIndex.rebuild`. Tolerates a table that does not exist yet.
149
+ */
150
+ private _clear;
135
151
  /** {@inheritDoc IFragmentVectorIndex.query} */
136
152
  query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
137
153
  /**
@@ -260,6 +276,8 @@ export declare class SqliteVecVectorIndex implements IVectorIndex {
260
276
  static create(params: ISqliteVecVectorIndexCreateParams): Promise<Result<SqliteVecVectorIndex>>;
261
277
  /** {@inheritDoc IVectorIndex.add} */
262
278
  add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>>;
279
+ /** {@inheritDoc IVectorIndex.has} */
280
+ has(target: IEdgeTarget): Promise<Result<boolean>>;
263
281
  /** {@inheritDoc IVectorIndex.remove} */
264
282
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
265
283
  /**
@@ -275,13 +293,14 @@ export declare class SqliteVecVectorIndex implements IVectorIndex {
275
293
  * a process kill mid-rebuild leaves the table holding neither the old index nor
276
294
  * the complete new one, and the remedy is to run `rebuild` again.
277
295
  */
278
- rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder, options?: IVectorRebuildOptions): Promise<Result<IVectorRebuildReport>>;
296
+ rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IVectorRebuildReport, IVectorRebuildReport>>;
279
297
  /**
280
- * Empty the table. Deliberately does NOT drop it or forget the established
281
- * dimension: the `vec0` table's dimension is fixed at creation and a re-embed at
282
- * a different dimension needs a drop-and-re-index, which is a consumer decision
283
- * (see the package README on `vec0` schema changes), not something a rebuild
284
- * should do silently.
298
+ * **Empties the rows; does NOT release the table's declared dimension.** That
299
+ * is a `vec0` constraint rather than a choice the dimension is schema, and
300
+ * there is no `ALTER TABLE` for it so a rebuild at a new dimension fails
301
+ * here where it would succeed on the in-memory sibling, which forgets its
302
+ * dimension on reset. Changing dimension needs a drop-and-re-index; see the
303
+ * note on `IVectorIndex.rebuild`.
285
304
  */
286
305
  private _clear;
287
306
  /** {@inheritDoc IVectorIndex.query} */
@@ -0,0 +1,38 @@
1
+ import { Result } from '@fgv/ts-utils';
2
+ import { Kind } from '@fgv/ts-agent-memory';
3
+ /**
4
+ * Invoke a consumer-supplied hook that already returns a `Result`, converting a
5
+ * synchronous throw or a promise rejection into a `Failure` rather than letting
6
+ * it escape.
7
+ *
8
+ * @remarks
9
+ * Package-internal. `@fgv/ts-agent-memory` carries an identical private copy for
10
+ * its in-memory indexes. Exporting a single `AsyncDeferredResult`-invoking
11
+ * primitive from `ts-utils` is the right home and is recorded in
12
+ * `docs/TECH_DEBT.md`; this module exists because *both* index classes in *this*
13
+ * package now need it, which is the point at which a second in-package copy stops
14
+ * being the cheaper thing.
15
+ */
16
+ export declare function invokeHook<T>(hook: () => Promise<Result<T>>): Promise<Result<T>>;
17
+ /**
18
+ * Compose the failure that aborted a rebuild with the outcome of the rollback
19
+ * that followed it.
20
+ *
21
+ * @remarks
22
+ * A rollback that ALSO fails is worth saying out loud: the `'fail'` path promises
23
+ * an empty index, and a caller that retries against a table which is neither the
24
+ * old index nor empty is working from a state the contract never described. This
25
+ * matters more here than in the in-memory package — these tables are **durable**,
26
+ * so a botched rollback survives the process.
27
+ */
28
+ export declare function withRollbackNote(error: string, rollback: Result<true>): string;
29
+ /**
30
+ * Increment `kind`'s tally by `by` (default one).
31
+ *
32
+ * @remarks
33
+ * The `by` parameter exists for the fragment lane, whose `fragments` count
34
+ * accumulates a fan-out rather than a record count — the one place a rebuild adds
35
+ * more than one per record.
36
+ */
37
+ export declare function tally(counts: Map<Kind, number>, kind: Kind, by?: number): void;
38
+ //# sourceMappingURL=rebuildHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebuildHelpers.d.ts","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/rebuildHelpers.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAsB,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAE5C;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAEtF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAE9E;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,GAAE,MAAU,GAAG,IAAI,CAEjF"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.invokeHook = invokeHook;
8
+ exports.withRollbackNote = withRollbackNote;
9
+ exports.tally = tally;
10
+ const ts_utils_1 = require("@fgv/ts-utils");
11
+ /**
12
+ * Invoke a consumer-supplied hook that already returns a `Result`, converting a
13
+ * synchronous throw or a promise rejection into a `Failure` rather than letting
14
+ * it escape.
15
+ *
16
+ * @remarks
17
+ * Package-internal. `@fgv/ts-agent-memory` carries an identical private copy for
18
+ * its in-memory indexes. Exporting a single `AsyncDeferredResult`-invoking
19
+ * primitive from `ts-utils` is the right home and is recorded in
20
+ * `docs/TECH_DEBT.md`; this module exists because *both* index classes in *this*
21
+ * package now need it, which is the point at which a second in-package copy stops
22
+ * being the cheaper thing.
23
+ */
24
+ async function invokeHook(hook) {
25
+ return (await (0, ts_utils_1.captureAsyncResult)(hook)).onSuccess((inner) => inner);
26
+ }
27
+ /**
28
+ * Compose the failure that aborted a rebuild with the outcome of the rollback
29
+ * that followed it.
30
+ *
31
+ * @remarks
32
+ * A rollback that ALSO fails is worth saying out loud: the `'fail'` path promises
33
+ * an empty index, and a caller that retries against a table which is neither the
34
+ * old index nor empty is working from a state the contract never described. This
35
+ * matters more here than in the in-memory package — these tables are **durable**,
36
+ * so a botched rollback survives the process.
37
+ */
38
+ function withRollbackNote(error, rollback) {
39
+ return rollback.isFailure() ? `${error} (rollback also failed: ${rollback.message})` : error;
40
+ }
41
+ /**
42
+ * Increment `kind`'s tally by `by` (default one).
43
+ *
44
+ * @remarks
45
+ * The `by` parameter exists for the fragment lane, whose `fragments` count
46
+ * accumulates a fan-out rather than a record count — the one place a rebuild adds
47
+ * more than one per record.
48
+ */
49
+ function tally(counts, kind, by = 1) {
50
+ var _a;
51
+ counts.set(kind, ((_a = counts.get(kind)) !== null && _a !== void 0 ? _a : 0) + by);
52
+ }
53
+ //# sourceMappingURL=rebuildHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebuildHelpers.js","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/rebuildHelpers.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAkBH,gCAEC;AAaD,4CAEC;AAUD,sBAEC;AA7CD,4CAA2D;AAG3D;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,UAAU,CAAI,IAA8B;IAChE,OAAO,CAAC,MAAM,IAAA,6BAAkB,EAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,gBAAgB,CAAC,KAAa,EAAE,QAAsB;IACpE,OAAO,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,2BAA2B,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;AAC/F,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,KAAK,CAAC,MAAyB,EAAE,IAAU,EAAE,KAAa,CAAC;;IACzE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAA,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AACjD,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, captureAsyncResult } from '@fgv/ts-utils';\nimport { Kind } from '@fgv/ts-agent-memory';\n\n/**\n * Invoke a consumer-supplied hook that already returns a `Result`, converting a\n * synchronous throw or a promise rejection into a `Failure` rather than letting\n * it escape.\n *\n * @remarks\n * Package-internal. `@fgv/ts-agent-memory` carries an identical private copy for\n * its in-memory indexes. Exporting a single `AsyncDeferredResult`-invoking\n * primitive from `ts-utils` is the right home and is recorded in\n * `docs/TECH_DEBT.md`; this module exists because *both* index classes in *this*\n * package now need it, which is the point at which a second in-package copy stops\n * being the cheaper thing.\n */\nexport async function invokeHook<T>(hook: () => Promise<Result<T>>): Promise<Result<T>> {\n return (await captureAsyncResult(hook)).onSuccess((inner) => inner);\n}\n\n/**\n * Compose the failure that aborted a rebuild with the outcome of the rollback\n * that followed it.\n *\n * @remarks\n * A rollback that ALSO fails is worth saying out loud: the `'fail'` path promises\n * an empty index, and a caller that retries against a table which is neither the\n * old index nor empty is working from a state the contract never described. This\n * matters more here than in the in-memory package — these tables are **durable**,\n * so a botched rollback survives the process.\n */\nexport function withRollbackNote(error: string, rollback: Result<true>): string {\n return rollback.isFailure() ? `${error} (rollback also failed: ${rollback.message})` : error;\n}\n\n/**\n * Increment `kind`'s tally by `by` (default one).\n *\n * @remarks\n * The `by` parameter exists for the fragment lane, whose `fragments` count\n * accumulates a fan-out rather than a record count — the one place a rebuild adds\n * more than one per record.\n */\nexport function tally(counts: Map<Kind, number>, kind: Kind, by: number = 1): void {\n counts.set(kind, (counts.get(kind) ?? 0) + by);\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { Result } from '@fgv/ts-utils';
2
- import { IEdgeTarget, IEmbeddedFragment, IFragmentVectorIndex, IVectorQueryHit } from '@fgv/ts-agent-memory';
1
+ import { DetailedResult, Result } from '@fgv/ts-utils';
2
+ import { FragmentEmbedder, IEdgeTarget, IEmbeddedFragment, IFragmentVectorIndex, IFragmentVectorRebuildReport, IMemoryRecordSource, IVectorQueryHit, IVectorRebuildOptions } from '@fgv/ts-agent-memory';
3
3
  import { ISqliteVecFragmentIndexCreateParams } from './model';
4
4
  /**
5
5
  * A persistent, `sqlite-vec`-backed `IFragmentVectorIndex` (from
@@ -75,6 +75,19 @@ export declare class SqliteVecFragmentIndex implements IFragmentVectorIndex {
75
75
  addFragments(target: IEdgeTarget, fragments: ReadonlyArray<IEmbeddedFragment>): Promise<Result<number>>;
76
76
  /** {@inheritDoc IFragmentVectorIndex.remove} */
77
77
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
78
+ /** {@inheritDoc IFragmentVectorIndex.has} */
79
+ has(target: IEdgeTarget): Promise<Result<boolean>>;
80
+ /** {@inheritDoc IFragmentVectorIndex.rebuild} */
81
+ rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>>;
82
+ /**
83
+ * **Empties the rows; does NOT release the table's declared dimension.** That
84
+ * is a `vec0` constraint rather than a choice — the dimension is schema, and
85
+ * there is no `ALTER TABLE` for it — so a rebuild at a new dimension fails
86
+ * here where it would succeed on the in-memory sibling, which forgets its
87
+ * dimension on reset. Changing dimension needs a drop-and-re-index; see the
88
+ * note on `IVectorIndex.rebuild`. Tolerates a table that does not exist yet.
89
+ */
90
+ private _clear;
78
91
  /** {@inheritDoc IFragmentVectorIndex.query} */
79
92
  query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
80
93
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sqliteVecFragmentIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/sqliteVecFragmentIndex.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAgC,MAAM,eAAe,CAAC;AACrE,OAAO,EACL,WAAW,EACX,iBAAiB,EAEjB,oBAAoB,EACpB,eAAe,EAIhB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,mCAAmC,EAAE,MAAM,SAAS,CAAC;AAmD9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,sBAAuB,YAAW,oBAAoB;IACjE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,yFAAyF;IACzF,OAAO,CAAC,UAAU,CAAqB;IACvC,qFAAqF;IACrF,OAAO,CAAC,MAAM,CAAkC;IAEhD,OAAO;IAOP,yGAAyG;IACzG,IAAW,WAAW,IAAI,MAAM,CAO/B;IAED,kGAAkG;IAClG,IAAW,aAAa,IAAI,MAAM,CAKjC;IAED;;;;;;;;;;;;OAYG;WACW,MAAM,CAAC,MAAM,EAAE,mCAAmC,GAAG,OAAO,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAmB1G,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAuE1B,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAahE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAsDlD;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAQpB,4EAA4E;IAC5E,OAAO,CAAC,QAAQ;IA0ChB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAsBrC;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAiBtC;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAa1B;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAiBzB,sHAAsH;IACtH,OAAO,CAAC,MAAM,CAAC,OAAO;IAItB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAWxB;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;CASzB"}
1
+ {"version":3,"file":"sqliteVecFragmentIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/sqlite-vec-index/sqliteVecFragmentIndex.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,cAAc,EACd,MAAM,EAMP,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,iBAAiB,EAEjB,oBAAoB,EACpB,4BAA4B,EAE5B,mBAAmB,EAEnB,eAAe,EACf,qBAAqB,EAKtB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,mCAAmC,EAAE,MAAM,SAAS,CAAC;AAmD9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,sBAAuB,YAAW,oBAAoB;IACjE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,yFAAyF;IACzF,OAAO,CAAC,UAAU,CAAqB;IACvC,qFAAqF;IACrF,OAAO,CAAC,MAAM,CAAkC;IAEhD,OAAO;IAOP,yGAAyG;IACzG,IAAW,WAAW,IAAI,MAAM,CAO/B;IAED,kGAAkG;IAClG,IAAW,aAAa,IAAI,MAAM,CAKjC;IAED;;;;;;;;;;;;OAYG;WACW,MAAM,CAAC,MAAM,EAAE,mCAAmC,GAAG,OAAO,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAmB1G,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAuE1B,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAahE,6CAA6C;IACtC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAazD,iDAAiD;IACpC,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,4BAA4B,EAAE,4BAA4B,CAAC,CAAC;IAqEtF;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM;IAWd,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAsDlD;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAQpB,4EAA4E;IAC5E,OAAO,CAAC,QAAQ;IA4ChB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAsBrC;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAiBtC;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAa1B;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAiBzB,sHAAsH;IACtH,OAAO,CAAC,MAAM,CAAC,OAAO;IAItB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAWxB;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;CASzB"}
@@ -8,6 +8,7 @@ exports.SqliteVecFragmentIndex = void 0;
8
8
  const sqlite_vec_1 = require("sqlite-vec");
9
9
  const ts_utils_1 = require("@fgv/ts-utils");
10
10
  const ts_agent_memory_1 = require("@fgv/ts-agent-memory");
11
+ const rebuildHelpers_1 = require("./rebuildHelpers");
11
12
  /** Default name for the fragment `vec0` virtual table. */
12
13
  const DEFAULT_TABLE_NAME = 'memory_fragments';
13
14
  /** A simple SQL identifier — the only shape allowed for the table name (it is interpolated into DDL). */
@@ -184,6 +185,101 @@ class SqliteVecFragmentIndex {
184
185
  return target;
185
186
  }).withErrorFormat((e) => `fragment index: cannot remove '${(0, ts_agent_memory_1.edgeTargetKey)(target)}': ${e}`));
186
187
  }
188
+ /** {@inheritDoc IFragmentVectorIndex.has} */
189
+ has(target) {
190
+ return Promise.resolve((0, ts_utils_1.captureResult)(() => {
191
+ // Before any add has created the table there is nothing held — a truthful
192
+ // `false`, matching `remove`'s idempotence and the zero counts.
193
+ if (this._stmts === undefined) {
194
+ return false;
195
+ }
196
+ return this._stmts.has.get((0, ts_agent_memory_1.edgeTargetKey)(target)) !== undefined;
197
+ }).withErrorFormat((e) => `fragment index: cannot check '${(0, ts_agent_memory_1.edgeTargetKey)(target)}': ${e}`));
198
+ }
199
+ /** {@inheritDoc IFragmentVectorIndex.rebuild} */
200
+ async rebuild(source, embed, options) {
201
+ var _a;
202
+ const lenient = ((_a = options === null || options === void 0 ? void 0 : options.onRecordError) !== null && _a !== void 0 ? _a : 'fail') === 'skip';
203
+ // `source` is consumer-supplied, so a throw or rejection becomes a `Failure`
204
+ // here rather than escaping as an exception.
205
+ const listed = await (0, rebuildHelpers_1.invokeHook)(() => source.list());
206
+ if (listed.isFailure()) {
207
+ // Deliberately BEFORE any clear, matching both siblings: a failed list is no
208
+ // evidence about the fragments already held, and clearing here would destroy
209
+ // a healthy PERSISTED index over a transient read error. No detail — there is
210
+ // nothing this call disturbed to describe.
211
+ return (0, ts_utils_1.failWithDetail)(`fragment index rebuild: failed to list records: ${listed.message}`);
212
+ }
213
+ const cleared = this._clear();
214
+ if (cleared.isFailure()) {
215
+ // Also nothing established: the table still holds whatever it held.
216
+ return (0, ts_utils_1.failWithDetail)(`fragment index rebuild: failed to clear the index: ${cleared.message}`);
217
+ }
218
+ const indexed = new Map();
219
+ const fragments = new Map();
220
+ const declined = new Map();
221
+ const skipped = [];
222
+ // Absent stays absent — only the source knows whether it filtered anything.
223
+ const report = () => ({
224
+ indexed,
225
+ fragments,
226
+ declined,
227
+ excluded: listed.value.excluded,
228
+ skipped
229
+ });
230
+ for (const scoped of listed.value.records) {
231
+ const kind = scoped.record.envelope.kind;
232
+ // Capture-wrapped: an embedder that throws mid-loop would otherwise escape
233
+ // past the `'fail'` rollback below, leaving this DURABLE table holding a
234
+ // partial index that survives the process.
235
+ const embedded = await (0, rebuildHelpers_1.invokeHook)(() => embed(scoped.record));
236
+ if (embedded.isFailure()) {
237
+ const error = `fragment index rebuild: embedding '${(0, ts_agent_memory_1.edgeTargetKey)(scoped.target)}' failed: ${embedded.message}`;
238
+ if (!lenient) {
239
+ // A rollback that also fails is said out loud: the `'fail'` path
240
+ // promises an empty index, and on a DURABLE table a botched rollback
241
+ // survives the process.
242
+ return (0, ts_utils_1.failWithDetail)((0, rebuildHelpers_1.withRollbackNote)(error, this._clear()), report());
243
+ }
244
+ skipped.push({ target: scoped.target, error });
245
+ continue;
246
+ }
247
+ // An empty array is this lane's decline, and it is still WRITTEN — the
248
+ // whole-record-replace is what clears any stale fragments.
249
+ const added = await this.addFragments(scoped.target, embedded.value);
250
+ if (added.isFailure()) {
251
+ const error = `fragment index rebuild: ${added.message}`;
252
+ if (!lenient) {
253
+ return (0, ts_utils_1.failWithDetail)((0, rebuildHelpers_1.withRollbackNote)(error, this._clear()), report());
254
+ }
255
+ skipped.push({ target: scoped.target, error });
256
+ continue;
257
+ }
258
+ if (added.value === 0) {
259
+ (0, rebuildHelpers_1.tally)(declined, kind);
260
+ continue;
261
+ }
262
+ (0, rebuildHelpers_1.tally)(indexed, kind);
263
+ (0, rebuildHelpers_1.tally)(fragments, kind, added.value);
264
+ }
265
+ return (0, ts_utils_1.succeedWithDetail)(report());
266
+ }
267
+ /**
268
+ * **Empties the rows; does NOT release the table's declared dimension.** That
269
+ * is a `vec0` constraint rather than a choice — the dimension is schema, and
270
+ * there is no `ALTER TABLE` for it — so a rebuild at a new dimension fails
271
+ * here where it would succeed on the in-memory sibling, which forgets its
272
+ * dimension on reset. Changing dimension needs a drop-and-re-index; see the
273
+ * note on `IVectorIndex.rebuild`. Tolerates a table that does not exist yet.
274
+ */
275
+ _clear() {
276
+ if (this._stmts === undefined) {
277
+ return (0, ts_utils_1.succeed)(true);
278
+ }
279
+ // Capture-wrapped like every other statement path: a closed connection or an
280
+ // I/O error is a `Failure`, not an exception out of a `Result`-returning method.
281
+ return (0, ts_utils_1.captureResult)(() => this._db.prepare(`DELETE FROM "${this._table}"`).run()).onSuccess(() => (0, ts_utils_1.succeed)(true));
282
+ }
187
283
  /** {@inheritDoc IFragmentVectorIndex.query} */
188
284
  query(vector, topK, maxPerRecord) {
189
285
  if (topK <= 0 || this._stmts === undefined) {
@@ -263,7 +359,9 @@ class SqliteVecFragmentIndex {
263
359
  query: this._db.prepare(`SELECT target_key, start_off, end_off, fragment_id, distance FROM "${this._table}" ` +
264
360
  `WHERE embedding MATCH ? AND k = ?`),
265
361
  fragmentCount: this._db.prepare(`SELECT count(*) AS c FROM "${this._table}"`),
266
- recordCount: this._db.prepare(`SELECT count(DISTINCT target_key) AS c FROM "${this._table}"`)
362
+ recordCount: this._db.prepare(`SELECT count(DISTINCT target_key) AS c FROM "${this._table}"`),
363
+ // `LIMIT 1`: membership needs existence, not cardinality.
364
+ has: this._db.prepare(`SELECT 1 FROM "${this._table}" WHERE target_key = ? LIMIT 1`)
267
365
  };
268
366
  }
269
367
  /**