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