@camstack/system 1.2.53 → 1.2.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/alerts/alerts.addon.js +1 -1
  7. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  10. package/dist/builtins/console-logging/index.js +1 -1
  11. package/dist/builtins/console-logging/index.mjs +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  18. package/dist/builtins/hub-forwarder/index.js +1 -1
  19. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  24. package/dist/builtins/local-network/local-network.addon.js +1 -1
  25. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  26. package/dist/builtins/loki-logging/index.js +1 -1
  27. package/dist/builtins/loki-logging/index.mjs +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  30. package/dist/builtins/platform-probe/index.js +1 -1
  31. package/dist/builtins/platform-probe/index.mjs +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  34. package/dist/builtins/snapshot/index.js +1 -1
  35. package/dist/builtins/snapshot/index.mjs +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +17 -2
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +246 -409
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +246 -409
  41. package/dist/builtins/sqlite-storage/vector-index-shared.d.ts +47 -0
  42. package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +55 -0
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/system-config/system-config.addon.js +1 -1
  46. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-f_YvZJaQ.mjs → dist-BOpRqInW.mjs} +1 -15
  50. package/dist/{dist-DKJqb0c7.js → dist-CblJsHb-.js} +0 -20
  51. package/dist/index.js +1 -1
  52. package/dist/index.mjs +1 -1
  53. package/package.json +3 -2
  54. package/dist/builtins/sqlite-storage/vector-index.d.ts +0 -197
@@ -3,8 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DKJqb0c7.js");
6
+ const require_dist = require("../../dist-CblJsHb-.js");
7
7
  let node_crypto = require("node:crypto");
8
+ let node_module = require("node:module");
8
9
  let better_sqlite3 = require("better-sqlite3");
9
10
  better_sqlite3 = require_chunk.__toESM(better_sqlite3);
10
11
  //#region src/builtins/sqlite-storage/filter-compiler.ts
@@ -26,7 +27,7 @@ function fieldExprFor(field, shape) {
26
27
  if (shape.kvBlobColumn !== null) return `json_extract("${shape.kvBlobColumn}", '$.${field}')`;
27
28
  return null;
28
29
  }
29
- function compileFilter(filter, shape, mode, serialize) {
30
+ function compileFilter$1(filter, shape, mode, serialize) {
30
31
  const clauses = [];
31
32
  const params = [];
32
33
  const resolve = (field) => {
@@ -254,7 +255,7 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
254
255
  async deleteWhere({ namespace, collection, filter }) {
255
256
  const scoped = this.scopedName(namespace, collection);
256
257
  const decl = this.requireDeclared(scoped);
257
- const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
258
+ const { whereSql, params } = compileFilter$1(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
258
259
  return { deleted: this.getDb().prepare(`DELETE FROM "${scoped}"${whereSql}`).run(...params).changes };
259
260
  }
260
261
  /**
@@ -277,7 +278,7 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
277
278
  setValues.push(this.serializeColumnValue(value));
278
279
  }
279
280
  if (setClauses.length === 0) throw new UnsafeFilterError("updateWhere was given no fields to set");
280
- const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
281
+ const { whereSql, params } = compileFilter$1(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
281
282
  return { updated: this.getDb().prepare(`UPDATE "${scoped}" SET ${setClauses.join(", ")}${whereSql}`).run(...setValues, ...params).changes };
282
283
  }
283
284
  async count({ namespace, collection, filter }) {
@@ -310,7 +311,7 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
310
311
  const shape = this.shapeOf(decl);
311
312
  const col = fieldExprFor(field, shape);
312
313
  if (col === null) return [];
313
- const { whereSql: where, params } = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
314
+ const { whereSql: where, params } = compileFilter$1(filter, shape, "select", (v) => this.serializeColumnValue(v));
314
315
  const sql = `SELECT ${`CAST((${col} - ?) / ? AS INTEGER)`} AS bucket, COUNT(*) AS count FROM "${scoped}"${where} GROUP BY bucket ORDER BY bucket`;
315
316
  return this.getDb().prepare(sql).all(origin, bucketSize, ...params).map((r) => ({
316
317
  bucket: r.bucket,
@@ -327,7 +328,7 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
327
328
  let sql = `SELECT ${[`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ")} FROM "${table}"`;
328
329
  const params = [];
329
330
  const shape = this.shapeOf(decl);
330
- const compiled = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
331
+ const compiled = compileFilter$1(filter, shape, "select", (v) => this.serializeColumnValue(v));
331
332
  sql += compiled.whereSql;
332
333
  params.push(...compiled.params);
333
334
  if (filter?.orderBy) {
@@ -647,225 +648,119 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
647
648
  }
648
649
  };
649
650
  //#endregion
650
- //#region src/builtins/sqlite-storage/vector-index.ts
651
- /**
652
- * SQLite-backed vector index — compact rows, exhaustive cosine ranking.
653
- *
654
- * Lives INSIDE the sqlite-settings builtin, against `SqliteSettingsBackend`
655
- * directly, and that placement is the point. It first shipped as its own addon
656
- * and that cost two round trips per write — caller → the `settings-store` door
657
- * → the engine ([D44](../../../../../docs/decisions/adr-0044.md)) — plus a
658
- * whole runner process, to reach the very database this addon already holds
659
- * open. Here an upsert is a function call.
660
- *
661
- * Replaceability is unaffected: it is the CAPABILITY that makes the backend
662
- * swappable, not the package. An ANN provider registers `vector-store` from
663
- * wherever it likes and `setActiveSingleton` picks it.
664
- *
665
- * This is the DEFAULT `vector-store` provider, not the final one. It exists to
666
- * make a row cheap; replacing exhaustive scan with a real ANN index is a
667
- * different provider registered against the same capability.
668
- *
669
- * What it changes versus the per-store JSON columns it replaces:
670
- *
671
- * - the vector is a base64 Float32 TEXT column (~2.7 KB for 512 dims) instead
672
- * of a JSON array of 512 numbers (~11.7 KB measured on the live hub), and
673
- * decoding is a `readFloatLE` loop instead of `JSON.parse` of 512 tokens;
674
- * - filterable metadata is promoted to real columns, so `deviceId` /
675
- * `timestamp` / `className` prefilters are pushed into SQL and indexed
676
- * rather than applied after loading everything;
677
- * - a truncated scan is REPORTED (`truncated`), because a search that silently
678
- * considered a subset reads as "nothing else matched".
679
- *
680
- * Metadata that is not one of the promoted columns rides in a JSON blob and is
681
- * returned verbatim; it cannot be filtered on. That is deliberate — a filter a
682
- * backend cannot push down is a filter that gets forgotten.
683
- */
684
- /** Metadata keys promoted to real, indexable columns. */
651
+ //#region src/builtins/sqlite-storage/vector-index-shared.ts
652
+ /** Metadata keys promoted to real, indexable columns in every backend. */
685
653
  var PROMOTED_COLUMNS = [
686
654
  "deviceId",
687
655
  "timestamp",
688
656
  "className",
689
657
  "modelId"
690
658
  ];
691
- /** Ceiling on rows compared in one query, so a pathological index cannot wedge the runner. */
692
- var MAX_SCAN_ROWS = 2e5;
693
- var collectionFor = (index) => `vector-store:${index}`;
659
+ //#endregion
660
+ //#region src/builtins/sqlite-storage/vector-index-vec.ts
661
+ var tableFor = (index) => `vec_${index.replace(/[^a-zA-Z0-9_]/g, "_")}`;
662
+ var registryTable = "vec_indexes";
663
+ /** `vec0` speaks packed Float32; the capability speaks base64 of the same. */
664
+ function toBlob(base64) {
665
+ return Buffer.from(base64, "base64");
666
+ }
694
667
  /**
695
- * Where index declarations live.
668
+ * `vec0` metadata columns are NOT NULLABLE.
696
669
  *
697
- * They MUST be durable. The rows survive a restart in SQLite but the `dim` and
698
- * `metric` that make them queryable were only ever in memory, so after a hub
699
- * restart `query` and `stats` both threw "index was never declared" until some
700
- * caller happened to write — which meant semantic search was dead from boot
701
- * until the next track closed. Callers declare lazily (the dimension comes from
702
- * the active encoder, unknown at construction), so nothing re-declared on boot.
670
+ * Passing NULL fails the whole insert with "Expected text for TEXT metadata
671
+ * column received NULL", and our metadata is genuinely optional a vector
672
+ * may carry no className or modelId. So absence is encoded with a sentinel and
673
+ * stripped again on read:
674
+ *
675
+ * - text → `''`. No real className or modelId is the empty string, so the
676
+ * round-trip is lossless.
677
+ * - integer → `MISSING_INT`. Chosen NEGATIVE because a device id and a
678
+ * millisecond timestamp are both non-negative in this system; 0 would be a
679
+ * plausible timestamp (1970) and could not be told from an absent one.
703
680
  */
704
- var INDEX_REGISTRY_COLLECTION = "vector-store:__indexes";
705
- var SqliteVectorIndex = class {
706
- store;
681
+ var MISSING_INT = -1;
682
+ /**
683
+ * Bind an integer metadata value as BigInt.
684
+ *
685
+ * Not defensive: `better-sqlite3` binds a plain JS number as FLOAT and `vec0`
686
+ * refuses it outright for an INTEGER metadata column.
687
+ */
688
+ function asInteger(value) {
689
+ if (typeof value !== "number" || !Number.isFinite(value)) return BigInt(MISSING_INT);
690
+ return BigInt(Math.trunc(value));
691
+ }
692
+ function asText(value) {
693
+ return typeof value === "string" ? value : "";
694
+ }
695
+ /** The nullable read of a text column — `''` means the caller never set it. */
696
+ function readText(value) {
697
+ return typeof value === "string" && value.length > 0 ? value : null;
698
+ }
699
+ /** The nullable read of an integer column — the sentinel means absent. */
700
+ function readInt(value) {
701
+ const n = Number(value);
702
+ if (!Number.isFinite(n) || n === MISSING_INT) return null;
703
+ return n;
704
+ }
705
+ var SqliteVecVectorIndex = class {
706
+ db;
707
707
  logger;
708
708
  specs = /* @__PURE__ */ new Map();
709
709
  constructor(deps) {
710
- this.store = deps.store;
710
+ this.db = deps.db;
711
711
  this.logger = deps.logger;
712
712
  }
713
- /**
714
- * Load persisted index declarations. Call once at startup, BEFORE serving:
715
- * without it every method on an existing index throws until a write
716
- * re-declares it.
717
- */
713
+ /** Recover index declarations so an existing table is usable from boot. */
718
714
  async loadIndexRegistry() {
719
715
  try {
720
- await this.store.declareCollection({
721
- collection: INDEX_REGISTRY_COLLECTION,
722
- columns: [
723
- {
724
- name: "id",
725
- type: "TEXT",
726
- primaryKey: true,
727
- notNull: true
728
- },
729
- {
730
- name: "dim",
731
- type: "INTEGER",
732
- notNull: true
733
- },
734
- {
735
- name: "metric",
736
- type: "TEXT",
737
- notNull: true
738
- }
739
- ]
740
- });
741
- const rows = await this.store.query({ collection: INDEX_REGISTRY_COLLECTION });
742
- for (const row of rows) {
743
- const dim = row.data["dim"];
744
- const metric = row.data["metric"];
745
- if (typeof dim !== "number") continue;
746
- await this.declareIndex(row.id, dim, isMetric(metric) ? metric : "cosine");
716
+ this.db.exec(`CREATE TABLE IF NOT EXISTS ${registryTable} (id TEXT PRIMARY KEY, dim INTEGER NOT NULL, metric TEXT NOT NULL)`);
717
+ const rows = this.db.prepare(`SELECT id, dim, metric FROM ${registryTable}`).all();
718
+ for (const raw of rows) {
719
+ if (typeof raw !== "object" || raw === null) continue;
720
+ const row = raw;
721
+ const id = asText(row["id"]);
722
+ const dim = typeof row["dim"] === "number" ? row["dim"] : Number(row["dim"]);
723
+ if (id === null || !Number.isFinite(dim)) continue;
724
+ this.specs.set(id, {
725
+ dim,
726
+ metric: metricOf(row["metric"])
727
+ });
747
728
  }
748
- this.logger.info("vector-store index registry loaded", { meta: { indexes: this.specs.size } });
729
+ this.logger.info("sqlite-vec index registry loaded", { meta: { indexes: this.specs.size } });
749
730
  } catch (err) {
750
- this.logger.error("vector-store index registry failed to load", { meta: { error: String(err) } });
731
+ this.logger.error("sqlite-vec registry failed to load", { meta: { error: String(err) } });
751
732
  }
752
733
  }
753
- /**
754
- * Idempotent. Re-declaring an index with a DIFFERENT dim throws rather than
755
- * wiping: silently dropping a populated index because a caller shipped a new
756
- * model would destroy history that cannot be rebuilt.
757
- */
758
734
  async declareIndex(index, dim, metric) {
759
735
  const existing = this.specs.get(index);
760
736
  if (existing !== void 0 && existing.dim !== dim) throw new Error(`vector-store: index "${index}" is declared with dim ${existing.dim}; refusing to redeclare as ${dim}`);
761
- await this.store.declareCollection({
762
- collection: collectionFor(index),
763
- columns: [
764
- {
765
- name: "id",
766
- type: "TEXT",
767
- primaryKey: true,
768
- notNull: true
769
- },
770
- {
771
- name: "vector",
772
- type: "TEXT",
773
- notNull: true
774
- },
775
- {
776
- name: "deviceId",
777
- type: "INTEGER"
778
- },
779
- {
780
- name: "timestamp",
781
- type: "INTEGER"
782
- },
783
- {
784
- name: "className",
785
- type: "TEXT"
786
- },
787
- {
788
- name: "modelId",
789
- type: "TEXT"
790
- },
791
- {
792
- name: "extra",
793
- type: "JSON"
794
- }
795
- ],
796
- indexes: [
797
- {
798
- name: `idx_vec_${index}_dev_ts`,
799
- columns: ["deviceId", "timestamp"]
800
- },
801
- {
802
- name: `idx_vec_${index}_ts`,
803
- columns: ["timestamp"]
804
- },
805
- {
806
- name: `idx_vec_${index}_class`,
807
- columns: ["className"]
808
- }
809
- ]
810
- });
737
+ this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${tableFor(index)} USING vec0(
738
+ id TEXT PRIMARY KEY,
739
+ embedding float[${dim}] distance_metric=${metric === "euclidean" ? "L2" : "cosine"},
740
+ deviceId integer,
741
+ timestamp integer,
742
+ className text,
743
+ modelId text,
744
+ +extra text
745
+ )`);
746
+ this.db.exec(`CREATE TABLE IF NOT EXISTS ${registryTable} (id TEXT PRIMARY KEY, dim INTEGER NOT NULL, metric TEXT NOT NULL)`);
747
+ this.db.prepare(`INSERT OR REPLACE INTO ${registryTable}(id, dim, metric) VALUES (?, ?, ?)`).run(index, BigInt(dim), metric);
811
748
  this.specs.set(index, {
812
749
  dim,
813
750
  metric
814
751
  });
815
- try {
816
- await this.store.set({
817
- collection: INDEX_REGISTRY_COLLECTION,
818
- key: index,
819
- value: {
820
- dim,
821
- metric
822
- }
823
- });
824
- } catch (err) {
825
- this.logger.warn("vector-store index declaration not persisted", { meta: {
826
- index,
827
- dim,
828
- error: String(err)
829
- } });
830
- }
831
752
  }
832
- /**
833
- * Resolve an index's spec, recovering it from stored rows when the registry
834
- * has no entry.
835
- *
836
- * The recovery is not belt-and-braces, it is required: indexes created before
837
- * the registry existed have rows and no declaration, and so would be
838
- * permanently unqueryable — which is exactly how this was found, with 8,494
839
- * live vectors that `stats` refused to look at. The dimension is derivable
840
- * from any stored vector, so derive it and persist it rather than demanding
841
- * the caller redeclare. The metric is NOT derivable and falls back to
842
- * `cosine`, which is what every current caller declares.
843
- */
844
- async resolveSpec(index) {
845
- const known = this.specs.get(index);
846
- if (known !== void 0) return known;
847
- const sample = (await this.store.query({
848
- collection: collectionFor(index),
849
- filter: { limit: 1 }
850
- }))[0];
851
- const vector = sample === void 0 ? void 0 : readRow(sample.data)?.vector;
852
- if (vector === void 0) throw new Error(`vector-store: index "${index}" was never declared`);
853
- const dim = require_dist.vectorDimFromBase64(vector);
854
- this.logger.warn("vector-store recovered an undeclared index from its rows", { meta: {
855
- index,
856
- dim,
857
- metric: "cosine"
858
- } });
859
- await this.declareIndex(index, dim, "cosine");
860
- return {
861
- dim,
862
- metric: "cosine"
863
- };
753
+ specOf(index) {
754
+ const spec = this.specs.get(index);
755
+ if (spec === void 0) throw new Error(`vector-store: index "${index}" was never declared`);
756
+ return spec;
864
757
  }
865
- /** Upsert items, refusing any whose vector length disagrees with the index. */
866
758
  async upsert(index, items) {
867
- const spec = await this.resolveSpec(index);
868
- const collection = collectionFor(index);
759
+ const spec = this.specOf(index);
760
+ const del = this.db.prepare(`DELETE FROM ${tableFor(index)} WHERE id = ?`);
761
+ const stmt = this.db.prepare(`INSERT INTO ${tableFor(index)}
762
+ (id, embedding, deviceId, timestamp, className, modelId, extra)
763
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
869
764
  let upserted = 0;
870
765
  let rejected = 0;
871
766
  for (const item of items) {
@@ -873,34 +768,22 @@ var SqliteVectorIndex = class {
873
768
  rejected += 1;
874
769
  continue;
875
770
  }
876
- const { deviceId, timestamp, className, modelId, extra } = splitMetadata(item.metadata);
877
771
  try {
878
- await this.store.set({
879
- collection,
880
- key: item.id,
881
- value: {
882
- vector: item.vector,
883
- deviceId,
884
- timestamp,
885
- className,
886
- modelId,
887
- extra
888
- }
889
- });
772
+ const extra = {};
773
+ for (const [k, v] of Object.entries(item.metadata)) if (!isPromoted(k)) extra[k] = v;
774
+ del.run(item.id);
775
+ stmt.run(item.id, toBlob(item.vector), asInteger(item.metadata["deviceId"]), asInteger(item.metadata["timestamp"]), asText(item.metadata["className"]), asText(item.metadata["modelId"]), JSON.stringify(extra));
890
776
  upserted += 1;
891
777
  } catch (err) {
892
778
  rejected += 1;
893
- this.logger.warn("vector-store upsert failed", {
894
- ...deviceId !== null ? { tags: { deviceId } } : {},
895
- meta: {
896
- index,
897
- id: item.id,
898
- error: String(err)
899
- }
900
- });
779
+ this.logger.warn("sqlite-vec upsert failed", { meta: {
780
+ index,
781
+ id: item.id,
782
+ error: String(err)
783
+ } });
901
784
  }
902
785
  }
903
- if (rejected > 0) this.logger.warn("vector-store rejected items on upsert", { meta: {
786
+ if (rejected > 0) this.logger.warn("sqlite-vec rejected items on upsert", { meta: {
904
787
  index,
905
788
  rejected,
906
789
  expectedDim: spec.dim
@@ -910,237 +793,168 @@ var SqliteVectorIndex = class {
910
793
  rejected
911
794
  };
912
795
  }
913
- /** Exhaustive ranked search over the rows a prefilter admits. */
914
796
  async query(params) {
915
- const spec = await this.resolveSpec(params.index);
916
- const queryVec = require_dist.decodeVectorBase64(params.vector);
917
- if (queryVec.length !== spec.dim) throw new Error(`vector-store: query vector has ${queryVec.length} dims, index "${params.index}" has ${spec.dim}`);
918
- const rows = await this.store.query({
919
- collection: collectionFor(params.index),
920
- filter: {
921
- ...buildSqlFilter(params.filter),
922
- orderBy: {
923
- field: "timestamp",
924
- direction: "desc"
925
- },
926
- limit: MAX_SCAN_ROWS
927
- }
928
- });
797
+ const spec = this.specOf(params.index);
798
+ if (require_dist.decodeVectorBase64(params.vector).length !== spec.dim) throw new Error(`vector-store: query vector has ${require_dist.decodeVectorBase64(params.vector).length} dims, index "${params.index}" has ${spec.dim}`);
799
+ const { clauses, values, unpushable } = compileFilter(params.filter);
800
+ const where = clauses.length > 0 ? ` AND ${clauses.join(" AND ")}` : "";
801
+ const rows = this.db.prepare(`SELECT id, distance, deviceId, timestamp, className, modelId, extra
802
+ FROM ${tableFor(params.index)}
803
+ WHERE embedding MATCH ? AND k = ?${where}
804
+ ORDER BY distance`).all(toBlob(params.vector), BigInt(params.topK), ...values);
929
805
  const minScore = params.minScore ?? Number.NEGATIVE_INFINITY;
930
- const scored = [];
931
- for (const row of rows) {
932
- const stored = readRow(row.data);
933
- if (stored === null) continue;
934
- if (!passesUnpushedFilter(stored, params.filter)) continue;
935
- const vec = require_dist.decodeVectorBase64(stored.vector);
936
- if (vec.length !== spec.dim) continue;
937
- const score = scoreFor(spec.metric, queryVec, vec);
806
+ const matches = [];
807
+ for (const raw of rows) {
808
+ if (typeof raw !== "object" || raw === null) continue;
809
+ const row = raw;
810
+ const id = asText(row["id"]);
811
+ const distance = typeof row["distance"] === "number" ? row["distance"] : Number(row["distance"]);
812
+ if (id === null || !Number.isFinite(distance)) continue;
813
+ const metadata = rebuild(row);
814
+ if (!passesUnpushable(metadata, unpushable)) continue;
815
+ const score = spec.metric === "euclidean" ? -distance : 1 - distance;
938
816
  if (score < minScore) continue;
939
- scored.push({
940
- id: row.id,
817
+ matches.push({
818
+ id,
941
819
  score,
942
- metadata: rebuildMetadata(stored)
820
+ metadata
943
821
  });
944
822
  }
945
- scored.sort((a, b) => b.score - a.score);
946
823
  return {
947
- matches: scored.slice(0, params.topK),
824
+ matches,
948
825
  scanned: rows.length,
949
- truncated: rows.length >= MAX_SCAN_ROWS
826
+ truncated: false
950
827
  };
951
828
  }
952
- /**
953
- * Metadata for the given ids, WITHOUT decoding their vectors.
954
- *
955
- * The caller is a best-of gate comparing confidences, so the vector is dead
956
- * weight on that path — reading it would reintroduce exactly the per-row cost
957
- * this index exists to remove. Missing ids are absent from the result rather
958
- * than present with an empty metadata object.
959
- */
960
829
  async getByIds(index, ids) {
961
- await this.resolveSpec(index);
830
+ this.specOf(index);
962
831
  if (ids.length === 0) return [];
963
- const rows = await this.store.query({
964
- collection: collectionFor(index),
965
- filter: { limit: MAX_SCAN_ROWS }
966
- });
967
- const wanted = new Set(ids);
832
+ const placeholders = ids.map(() => "?").join(",");
833
+ const rows = this.db.prepare(`SELECT id, deviceId, timestamp, className, modelId, extra
834
+ FROM ${tableFor(index)} WHERE id IN (${placeholders})`).all(...ids);
968
835
  const out = [];
969
- for (const row of rows) {
970
- if (!wanted.has(row.id)) continue;
971
- const stored = readRow(row.data);
972
- if (stored === null) continue;
836
+ for (const raw of rows) {
837
+ if (typeof raw !== "object" || raw === null) continue;
838
+ const row = raw;
839
+ const id = asText(row["id"]);
840
+ if (id === null) continue;
973
841
  out.push({
974
- id: row.id,
975
- metadata: rebuildMetadata(stored)
842
+ id,
843
+ metadata: rebuild(row)
976
844
  });
977
845
  }
978
846
  return out;
979
847
  }
980
848
  async deleteByIds(index, ids) {
981
- const collection = collectionFor(index);
982
- let deleted = 0;
983
- for (const id of ids) try {
984
- await this.store.delete({
985
- collection,
986
- key: id
987
- });
988
- deleted += 1;
989
- } catch (err) {
990
- this.logger.warn("vector-store delete failed", { meta: {
991
- index,
992
- id,
993
- error: String(err)
994
- } });
995
- }
996
- return deleted;
849
+ this.specOf(index);
850
+ if (ids.length === 0) return 0;
851
+ const placeholders = ids.map(() => "?").join(",");
852
+ return this.db.prepare(`DELETE FROM ${tableFor(index)} WHERE id IN (${placeholders})`).run(...ids).changes;
997
853
  }
998
- /**
999
- * Delete every row the filter admits.
1000
- *
1001
- * When every clause is pushable, this is ONE bulk statement. When the filter
1002
- * touches a non-promoted key it falls back to select-then-delete, because
1003
- * those clauses live in the JSON remainder and only JS can evaluate them —
1004
- * and an unapplied clause would delete rows the caller wanted kept, which is
1005
- * the one failure mode a delete must not have.
1006
- */
1007
854
  async deleteByFilter(index, filter) {
1008
- await this.resolveSpec(index);
1009
- const sqlFilter = buildSqlFilter(filter);
1010
- if (isFullyPushable(filter)) return (await this.store.deleteWhere({
1011
- collection: collectionFor(index),
1012
- filter: sqlFilter
1013
- })).deleted;
1014
- const ids = (await this.store.query({
1015
- collection: collectionFor(index),
1016
- filter: {
1017
- ...sqlFilter,
1018
- limit: MAX_SCAN_ROWS
1019
- }
1020
- })).filter((r) => {
1021
- const stored = readRow(r.data);
1022
- return stored !== null && passesUnpushedFilter(stored, filter);
1023
- }).map((r) => r.id);
1024
- return this.deleteByIds(index, ids);
855
+ this.specOf(index);
856
+ const { clauses, values, unpushable } = compileFilter(filter);
857
+ if (unpushable.length === 0) {
858
+ const where = clauses.length > 0 ? clauses.join(" AND ") : "1=1";
859
+ return this.db.prepare(`DELETE FROM ${tableFor(index)} WHERE ${where}`).run(...values).changes;
860
+ }
861
+ const rows = this.db.prepare(`SELECT id, deviceId, timestamp, className, modelId, extra FROM ${tableFor(index)}${clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : ""}`).all(...values);
862
+ const doomed = [];
863
+ for (const raw of rows) {
864
+ if (typeof raw !== "object" || raw === null) continue;
865
+ const row = raw;
866
+ const id = asText(row["id"]);
867
+ if (id !== null && passesUnpushable(rebuild(row), unpushable)) doomed.push(id);
868
+ }
869
+ return this.deleteByIds(index, doomed);
1025
870
  }
1026
871
  async stats(index) {
1027
- const spec = await this.resolveSpec(index);
872
+ const spec = this.specOf(index);
873
+ const row = this.db.prepare(`SELECT count(*) AS c FROM ${tableFor(index)}`).get();
874
+ const count = typeof row === "object" && row !== null ? Number(row["c"]) : 0;
1028
875
  return {
1029
- backend: "sqlite-bruteforce",
1030
- count: (await this.store.query({
1031
- collection: collectionFor(index),
1032
- filter: { limit: MAX_SCAN_ROWS }
1033
- })).length,
876
+ backend: "sqlite-vec",
877
+ count: Number.isFinite(count) ? count : 0,
1034
878
  dim: spec.dim,
1035
879
  metric: spec.metric,
1036
880
  exact: true
1037
881
  };
1038
882
  }
1039
883
  };
1040
- /** Split caller metadata into promoted columns plus a verbatim remainder. */
1041
- function splitMetadata(metadata) {
1042
- const extra = {};
1043
- for (const [key, value] of Object.entries(metadata)) if (!isPromoted(key)) extra[key] = value;
1044
- return {
1045
- deviceId: numberOrNull(metadata["deviceId"]),
1046
- timestamp: numberOrNull(metadata["timestamp"]),
1047
- className: stringOrNull(metadata["className"]),
1048
- modelId: stringOrNull(metadata["modelId"]),
1049
- extra
1050
- };
1051
- }
1052
884
  function isPromoted(key) {
1053
- for (const promoted of PROMOTED_COLUMNS) if (promoted === key) return true;
885
+ for (const p of PROMOTED_COLUMNS) if (p === key) return true;
1054
886
  return false;
1055
887
  }
1056
- function numberOrNull(value) {
1057
- return typeof value === "number" ? value : null;
1058
- }
1059
- function stringOrNull(value) {
1060
- return typeof value === "string" ? value : null;
888
+ function metricOf(value) {
889
+ return value === "euclidean" || value === "dot" ? value : "cosine";
1061
890
  }
1062
- /** Rebuild the caller's metadata from the promoted columns plus the remainder. */
1063
- function rebuildMetadata(row) {
1064
- const out = { ...row.extra };
1065
- if (row.deviceId !== null) out["deviceId"] = row.deviceId;
1066
- if (row.timestamp !== null) out["timestamp"] = row.timestamp;
1067
- if (row.className !== null) out["className"] = row.className;
1068
- if (row.modelId !== null) out["modelId"] = row.modelId;
1069
- return out;
1070
- }
1071
- /** The part of a filter SQLite can apply: promoted columns only. */
1072
- function buildSqlFilter(filter) {
1073
- if (filter === void 0) return {};
1074
- const where = {};
1075
- const whereBetween = {};
1076
- for (const [key, value] of Object.entries(filter.equals ?? {})) if (isPromoted(key)) where[key] = value;
1077
- for (const [key, range] of Object.entries(filter.between ?? {})) if (isPromoted(key)) whereBetween[key] = range;
1078
- return {
1079
- ...Object.keys(where).length > 0 ? { where } : {},
1080
- ...Object.keys(whereBetween).length > 0 ? { whereBetween } : {}
891
+ /** Split a filter into SQL clauses and the remainder JS must apply. */
892
+ function compileFilter(filter) {
893
+ const clauses = [];
894
+ const values = [];
895
+ const unpushable = [];
896
+ if (filter === void 0) return {
897
+ clauses,
898
+ values,
899
+ unpushable
1081
900
  };
1082
- }
1083
- /**
1084
- * Apply the clauses SQL could not.
1085
- *
1086
- * A filter key that is not a promoted column lives inside the `extra` JSON
1087
- * blob, so it is checked here in JS. It is applied rather than ignored — an
1088
- * unapplied filter returns rows the caller will treat as matches.
1089
- */
1090
- function passesUnpushedFilter(row, filter) {
1091
- if (filter === void 0) return true;
1092
- for (const [key, expected] of Object.entries(filter.equals ?? {})) {
1093
- if (isPromoted(key)) continue;
1094
- if (row.extra[key] !== expected) return false;
901
+ for (const [key, value] of Object.entries(filter.equals ?? {})) {
902
+ if (!isPromoted(key)) {
903
+ unpushable.push({
904
+ key,
905
+ equals: value
906
+ });
907
+ continue;
908
+ }
909
+ clauses.push(`${key} = ?`);
910
+ values.push(typeof value === "number" ? BigInt(Math.trunc(value)) : value);
1095
911
  }
1096
912
  for (const [key, [lo, hi]] of Object.entries(filter.between ?? {})) {
1097
- if (isPromoted(key)) continue;
1098
- const actual = row.extra[key];
1099
- if (typeof actual !== "number" || actual < lo || actual > hi) return false;
913
+ if (!isPromoted(key)) {
914
+ unpushable.push({
915
+ key,
916
+ between: [lo, hi]
917
+ });
918
+ continue;
919
+ }
920
+ clauses.push(`${key} BETWEEN ? AND ?`);
921
+ values.push(BigInt(Math.trunc(lo)), BigInt(Math.trunc(hi)));
1100
922
  }
1101
- return true;
1102
- }
1103
- function readRow(data) {
1104
- const vector = data["vector"];
1105
- if (typeof vector !== "string" || vector.length === 0) return null;
1106
- const extra = data["extra"];
1107
923
  return {
1108
- vector,
1109
- deviceId: numberOrNull(data["deviceId"]),
1110
- timestamp: numberOrNull(data["timestamp"]),
1111
- className: stringOrNull(data["className"]),
1112
- modelId: stringOrNull(data["modelId"]),
1113
- extra: isMetadata(extra) ? extra : {}
924
+ clauses,
925
+ values,
926
+ unpushable
1114
927
  };
1115
928
  }
1116
- function isMetadata(value) {
1117
- return typeof value === "object" && value !== null && !Array.isArray(value);
1118
- }
1119
- /** True when every clause of the filter maps to a promoted column. */
1120
- function isFullyPushable(filter) {
1121
- for (const key of Object.keys(filter.equals ?? {})) if (!isPromoted(key)) return false;
1122
- for (const key of Object.keys(filter.between ?? {})) if (!isPromoted(key)) return false;
929
+ function passesUnpushable(metadata, unpushable) {
930
+ for (const clause of unpushable) {
931
+ const actual = metadata[clause.key];
932
+ if (clause.equals !== void 0 && actual !== clause.equals) return false;
933
+ if (clause.between !== void 0) {
934
+ const [lo, hi] = clause.between;
935
+ if (typeof actual !== "number" || actual < lo || actual > hi) return false;
936
+ }
937
+ }
1123
938
  return true;
1124
939
  }
1125
- /** Narrow a stored metric string back to the enum. */
1126
- function isMetric(value) {
1127
- return value === "cosine" || value === "dot" || value === "euclidean";
1128
- }
1129
- /** Score two vectors under the index's metric. Higher is nearer in every case. */
1130
- function scoreFor(metric, a, b) {
1131
- if (metric === "cosine") return require_dist.cosineSimilarity(a, b);
1132
- if (metric === "dot") {
1133
- let sum = 0;
1134
- for (let i = 0; i < a.length; i += 1) sum += (a[i] ?? 0) * (b[i] ?? 0);
1135
- return sum;
1136
- }
1137
- let sum = 0;
1138
- for (let i = 0; i < a.length; i += 1) {
1139
- const d = (a[i] ?? 0) - (b[i] ?? 0);
1140
- sum += d * d;
1141
- }
1142
- const distance = Math.sqrt(sum);
1143
- return distance === 0 ? 0 : -distance;
940
+ /** Rebuild caller metadata from the promoted columns plus the JSON remainder. */
941
+ function rebuild(row) {
942
+ let extra = {};
943
+ const rawExtra = row["extra"];
944
+ if (typeof rawExtra === "string" && rawExtra.length > 0) try {
945
+ const parsed = JSON.parse(rawExtra);
946
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) extra = parsed;
947
+ } catch {}
948
+ const out = { ...extra };
949
+ const deviceId = readInt(row["deviceId"]);
950
+ const timestamp = readInt(row["timestamp"]);
951
+ const className = readText(row["className"]);
952
+ const modelId = readText(row["modelId"]);
953
+ if (deviceId !== null) out["deviceId"] = deviceId;
954
+ if (timestamp !== null) out["timestamp"] = timestamp;
955
+ if (className !== null) out["className"] = className;
956
+ if (modelId !== null) out["modelId"] = modelId;
957
+ return out;
1144
958
  }
1145
959
  //#endregion
1146
960
  //#region src/builtins/sqlite-storage/sqlite-settings.addon.ts
@@ -1186,10 +1000,7 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
1186
1000
  this.ctx.logger.info("DB file status", { meta: { dbExists } });
1187
1001
  this.backend = new SqliteSettingsBackend(dbPath, { ...require_dist.RUNTIME_DEFAULTS });
1188
1002
  await this.backend.initialize();
1189
- const vectorIndex = new SqliteVectorIndex({
1190
- store: this.backend,
1191
- logger: this.ctx.logger.child("VectorIndex")
1192
- });
1003
+ const vectorIndex = this.createVectorIndex(this.backend);
1193
1004
  await vectorIndex.loadIndexRegistry();
1194
1005
  this.vectorIndex = vectorIndex;
1195
1006
  const vectorProvider = {
@@ -1224,6 +1035,32 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
1224
1035
  getBackend() {
1225
1036
  return this.backend;
1226
1037
  }
1038
+ /**
1039
+ * Build the vector index.
1040
+ *
1041
+ * ONE implementation, deliberately. A second backend behind a flag means two
1042
+ * code paths that can disagree, two sets of gotchas, and a silent fallback
1043
+ * that turns "the extension is missing" into "search is mysteriously slow" —
1044
+ * the failure this codebase keeps paying for. The capability is the swap
1045
+ * point: another provider replaces this one wholesale, it does not hide
1046
+ * inside it.
1047
+ *
1048
+ * So a failed extension load THROWS. A node without the native binary must
1049
+ * fail at boot with a message naming sqlite-vec, not serve degraded results
1050
+ * nobody knows are degraded.
1051
+ */
1052
+ createVectorIndex(backend) {
1053
+ const logger = this.ctx.logger.child("VectorIndex");
1054
+ const db = backend.getDatabase();
1055
+ if (db === null) throw new Error("vector-store: the SQLite database is not open");
1056
+ const sv = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("sqlite-vec");
1057
+ db.loadExtension(sv.getLoadablePath());
1058
+ logger.info("sqlite-vec extension loaded");
1059
+ return new SqliteVecVectorIndex({
1060
+ db,
1061
+ logger
1062
+ });
1063
+ }
1227
1064
  /** Exposed for tests; null before `onInitialize`. */
1228
1065
  getVectorIndex() {
1229
1066
  return this.vectorIndex;