@camstack/system 1.2.47 → 1.2.48

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 (51) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  17. package/dist/builtins/hub-forwarder/index.js +1 -1
  18. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  19. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  23. package/dist/builtins/local-network/local-network.addon.js +1 -1
  24. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  25. package/dist/builtins/loki-logging/index.js +1 -1
  26. package/dist/builtins/loki-logging/index.mjs +1 -1
  27. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  29. package/dist/builtins/platform-probe/index.js +1 -1
  30. package/dist/builtins/platform-probe/index.mjs +1 -1
  31. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  33. package/dist/builtins/snapshot/index.js +1 -1
  34. package/dist/builtins/snapshot/index.mjs +1 -1
  35. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +10 -0
  38. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +516 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +516 -1
  40. package/dist/builtins/sqlite-storage/vector-index.d.ts +172 -0
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/system-config/system-config.addon.js +1 -1
  44. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  45. package/dist/builtins/winston-logging/index.js +1 -1
  46. package/dist/builtins/winston-logging/index.mjs +1 -1
  47. package/dist/{dist-BRBfh1_9.mjs → dist-C7rPTQLR.mjs} +33 -1
  48. package/dist/{dist-CKjrL1qP.js → dist-CH0DyAwh.js} +56 -0
  49. package/dist/index.js +1 -1
  50. package/dist/index.mjs +1 -1
  51. package/package.json +1 -1
@@ -3,7 +3,7 @@ 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-CKjrL1qP.js");
6
+ const require_dist = require("../../dist-CH0DyAwh.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let better_sqlite3 = require("better-sqlite3");
9
9
  better_sqlite3 = require_chunk.__toESM(better_sqlite3);
@@ -647,6 +647,484 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
647
647
  }
648
648
  };
649
649
  //#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. */
685
+ var PROMOTED_COLUMNS = [
686
+ "deviceId",
687
+ "timestamp",
688
+ "className",
689
+ "modelId"
690
+ ];
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}`;
694
+ /**
695
+ * Where index declarations live.
696
+ *
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.
703
+ */
704
+ var INDEX_REGISTRY_COLLECTION = "vector-store:__indexes";
705
+ var SqliteVectorIndex = class {
706
+ store;
707
+ logger;
708
+ specs = /* @__PURE__ */ new Map();
709
+ constructor(deps) {
710
+ this.store = deps.store;
711
+ this.logger = deps.logger;
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
+ */
718
+ async loadIndexRegistry() {
719
+ 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
+ this.specs.set(row.id, {
747
+ dim,
748
+ metric: isMetric(metric) ? metric : "cosine"
749
+ });
750
+ }
751
+ this.logger.info("vector-store index registry loaded", { meta: { indexes: this.specs.size } });
752
+ } catch (err) {
753
+ this.logger.error("vector-store index registry failed to load", { meta: { error: String(err) } });
754
+ }
755
+ }
756
+ /**
757
+ * Idempotent. Re-declaring an index with a DIFFERENT dim throws rather than
758
+ * wiping: silently dropping a populated index because a caller shipped a new
759
+ * model would destroy history that cannot be rebuilt.
760
+ */
761
+ async declareIndex(index, dim, metric) {
762
+ const existing = this.specs.get(index);
763
+ 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}`);
764
+ await this.store.declareCollection({
765
+ collection: collectionFor(index),
766
+ columns: [
767
+ {
768
+ name: "id",
769
+ type: "TEXT",
770
+ primaryKey: true,
771
+ notNull: true
772
+ },
773
+ {
774
+ name: "vector",
775
+ type: "TEXT",
776
+ notNull: true
777
+ },
778
+ {
779
+ name: "deviceId",
780
+ type: "INTEGER"
781
+ },
782
+ {
783
+ name: "timestamp",
784
+ type: "INTEGER"
785
+ },
786
+ {
787
+ name: "className",
788
+ type: "TEXT"
789
+ },
790
+ {
791
+ name: "modelId",
792
+ type: "TEXT"
793
+ },
794
+ {
795
+ name: "extra",
796
+ type: "JSON"
797
+ }
798
+ ],
799
+ indexes: [
800
+ {
801
+ name: `idx_vec_${index}_dev_ts`,
802
+ columns: ["deviceId", "timestamp"]
803
+ },
804
+ {
805
+ name: `idx_vec_${index}_ts`,
806
+ columns: ["timestamp"]
807
+ },
808
+ {
809
+ name: `idx_vec_${index}_class`,
810
+ columns: ["className"]
811
+ }
812
+ ]
813
+ });
814
+ this.specs.set(index, {
815
+ dim,
816
+ metric
817
+ });
818
+ try {
819
+ await this.store.set({
820
+ collection: INDEX_REGISTRY_COLLECTION,
821
+ key: index,
822
+ value: {
823
+ dim,
824
+ metric
825
+ }
826
+ });
827
+ } catch (err) {
828
+ this.logger.warn("vector-store index declaration not persisted", { meta: {
829
+ index,
830
+ dim,
831
+ error: String(err)
832
+ } });
833
+ }
834
+ }
835
+ /**
836
+ * Resolve an index's spec, recovering it from stored rows when the registry
837
+ * has no entry.
838
+ *
839
+ * The recovery is not belt-and-braces, it is required: indexes created before
840
+ * the registry existed have rows and no declaration, and so would be
841
+ * permanently unqueryable — which is exactly how this was found, with 8,494
842
+ * live vectors that `stats` refused to look at. The dimension is derivable
843
+ * from any stored vector, so derive it and persist it rather than demanding
844
+ * the caller redeclare. The metric is NOT derivable and falls back to
845
+ * `cosine`, which is what every current caller declares.
846
+ */
847
+ async resolveSpec(index) {
848
+ const known = this.specs.get(index);
849
+ if (known !== void 0) return known;
850
+ const sample = (await this.store.query({
851
+ collection: collectionFor(index),
852
+ filter: { limit: 1 }
853
+ }))[0];
854
+ const vector = sample === void 0 ? void 0 : readRow(sample.data)?.vector;
855
+ if (vector === void 0) throw new Error(`vector-store: index "${index}" was never declared`);
856
+ const dim = require_dist.vectorDimFromBase64(vector);
857
+ this.logger.warn("vector-store recovered an undeclared index from its rows", { meta: {
858
+ index,
859
+ dim,
860
+ metric: "cosine"
861
+ } });
862
+ await this.declareIndex(index, dim, "cosine");
863
+ return {
864
+ dim,
865
+ metric: "cosine"
866
+ };
867
+ }
868
+ /** Upsert items, refusing any whose vector length disagrees with the index. */
869
+ async upsert(index, items) {
870
+ const spec = await this.resolveSpec(index);
871
+ const collection = collectionFor(index);
872
+ let upserted = 0;
873
+ let rejected = 0;
874
+ for (const item of items) {
875
+ if (require_dist.vectorDimFromBase64(item.vector) !== spec.dim) {
876
+ rejected += 1;
877
+ continue;
878
+ }
879
+ const { deviceId, timestamp, className, modelId, extra } = splitMetadata(item.metadata);
880
+ try {
881
+ await this.store.set({
882
+ collection,
883
+ key: item.id,
884
+ value: {
885
+ vector: item.vector,
886
+ deviceId,
887
+ timestamp,
888
+ className,
889
+ modelId,
890
+ extra
891
+ }
892
+ });
893
+ upserted += 1;
894
+ } catch (err) {
895
+ rejected += 1;
896
+ this.logger.warn("vector-store upsert failed", {
897
+ ...deviceId !== null ? { tags: { deviceId } } : {},
898
+ meta: {
899
+ index,
900
+ id: item.id,
901
+ error: String(err)
902
+ }
903
+ });
904
+ }
905
+ }
906
+ if (rejected > 0) this.logger.warn("vector-store rejected items on upsert", { meta: {
907
+ index,
908
+ rejected,
909
+ expectedDim: spec.dim
910
+ } });
911
+ return {
912
+ upserted,
913
+ rejected
914
+ };
915
+ }
916
+ /** Exhaustive ranked search over the rows a prefilter admits. */
917
+ async query(params) {
918
+ const spec = await this.resolveSpec(params.index);
919
+ const queryVec = require_dist.decodeVectorBase64(params.vector);
920
+ if (queryVec.length !== spec.dim) throw new Error(`vector-store: query vector has ${queryVec.length} dims, index "${params.index}" has ${spec.dim}`);
921
+ const rows = await this.store.query({
922
+ collection: collectionFor(params.index),
923
+ filter: {
924
+ ...buildSqlFilter(params.filter),
925
+ orderBy: {
926
+ field: "timestamp",
927
+ direction: "desc"
928
+ },
929
+ limit: MAX_SCAN_ROWS
930
+ }
931
+ });
932
+ const minScore = params.minScore ?? Number.NEGATIVE_INFINITY;
933
+ const scored = [];
934
+ for (const row of rows) {
935
+ const stored = readRow(row.data);
936
+ if (stored === null) continue;
937
+ if (!passesUnpushedFilter(stored, params.filter)) continue;
938
+ const vec = require_dist.decodeVectorBase64(stored.vector);
939
+ if (vec.length !== spec.dim) continue;
940
+ const score = scoreFor(spec.metric, queryVec, vec);
941
+ if (score < minScore) continue;
942
+ scored.push({
943
+ id: row.id,
944
+ score,
945
+ metadata: rebuildMetadata(stored)
946
+ });
947
+ }
948
+ scored.sort((a, b) => b.score - a.score);
949
+ return {
950
+ matches: scored.slice(0, params.topK),
951
+ scanned: rows.length,
952
+ truncated: rows.length >= MAX_SCAN_ROWS
953
+ };
954
+ }
955
+ /**
956
+ * Metadata for the given ids, WITHOUT decoding their vectors.
957
+ *
958
+ * The caller is a best-of gate comparing confidences, so the vector is dead
959
+ * weight on that path — reading it would reintroduce exactly the per-row cost
960
+ * this index exists to remove. Missing ids are absent from the result rather
961
+ * than present with an empty metadata object.
962
+ */
963
+ async getByIds(index, ids) {
964
+ await this.resolveSpec(index);
965
+ if (ids.length === 0) return [];
966
+ const rows = await this.store.query({
967
+ collection: collectionFor(index),
968
+ filter: { limit: MAX_SCAN_ROWS }
969
+ });
970
+ const wanted = new Set(ids);
971
+ const out = [];
972
+ for (const row of rows) {
973
+ if (!wanted.has(row.id)) continue;
974
+ const stored = readRow(row.data);
975
+ if (stored === null) continue;
976
+ out.push({
977
+ id: row.id,
978
+ metadata: rebuildMetadata(stored)
979
+ });
980
+ }
981
+ return out;
982
+ }
983
+ async deleteByIds(index, ids) {
984
+ const collection = collectionFor(index);
985
+ let deleted = 0;
986
+ for (const id of ids) try {
987
+ await this.store.delete({
988
+ collection,
989
+ key: id
990
+ });
991
+ deleted += 1;
992
+ } catch (err) {
993
+ this.logger.warn("vector-store delete failed", { meta: {
994
+ index,
995
+ id,
996
+ error: String(err)
997
+ } });
998
+ }
999
+ return deleted;
1000
+ }
1001
+ async deleteByFilter(index, filter) {
1002
+ const ids = (await this.store.query({
1003
+ collection: collectionFor(index),
1004
+ filter: {
1005
+ ...buildSqlFilter(filter),
1006
+ limit: MAX_SCAN_ROWS
1007
+ }
1008
+ })).filter((r) => {
1009
+ const stored = readRow(r.data);
1010
+ return stored !== null && passesUnpushedFilter(stored, filter);
1011
+ }).map((r) => r.id);
1012
+ return this.deleteByIds(index, ids);
1013
+ }
1014
+ async stats(index) {
1015
+ const spec = await this.resolveSpec(index);
1016
+ return {
1017
+ backend: "sqlite-bruteforce",
1018
+ count: (await this.store.query({
1019
+ collection: collectionFor(index),
1020
+ filter: { limit: MAX_SCAN_ROWS }
1021
+ })).length,
1022
+ dim: spec.dim,
1023
+ metric: spec.metric,
1024
+ exact: true
1025
+ };
1026
+ }
1027
+ };
1028
+ /** Split caller metadata into promoted columns plus a verbatim remainder. */
1029
+ function splitMetadata(metadata) {
1030
+ const extra = {};
1031
+ for (const [key, value] of Object.entries(metadata)) if (!isPromoted(key)) extra[key] = value;
1032
+ return {
1033
+ deviceId: numberOrNull(metadata["deviceId"]),
1034
+ timestamp: numberOrNull(metadata["timestamp"]),
1035
+ className: stringOrNull(metadata["className"]),
1036
+ modelId: stringOrNull(metadata["modelId"]),
1037
+ extra
1038
+ };
1039
+ }
1040
+ function isPromoted(key) {
1041
+ for (const promoted of PROMOTED_COLUMNS) if (promoted === key) return true;
1042
+ return false;
1043
+ }
1044
+ function numberOrNull(value) {
1045
+ return typeof value === "number" ? value : null;
1046
+ }
1047
+ function stringOrNull(value) {
1048
+ return typeof value === "string" ? value : null;
1049
+ }
1050
+ /** Rebuild the caller's metadata from the promoted columns plus the remainder. */
1051
+ function rebuildMetadata(row) {
1052
+ const out = { ...row.extra };
1053
+ if (row.deviceId !== null) out["deviceId"] = row.deviceId;
1054
+ if (row.timestamp !== null) out["timestamp"] = row.timestamp;
1055
+ if (row.className !== null) out["className"] = row.className;
1056
+ if (row.modelId !== null) out["modelId"] = row.modelId;
1057
+ return out;
1058
+ }
1059
+ /** The part of a filter SQLite can apply: promoted columns only. */
1060
+ function buildSqlFilter(filter) {
1061
+ if (filter === void 0) return {};
1062
+ const where = {};
1063
+ const whereBetween = {};
1064
+ for (const [key, value] of Object.entries(filter.equals ?? {})) if (isPromoted(key)) where[key] = value;
1065
+ for (const [key, range] of Object.entries(filter.between ?? {})) if (isPromoted(key)) whereBetween[key] = range;
1066
+ return {
1067
+ ...Object.keys(where).length > 0 ? { where } : {},
1068
+ ...Object.keys(whereBetween).length > 0 ? { whereBetween } : {}
1069
+ };
1070
+ }
1071
+ /**
1072
+ * Apply the clauses SQL could not.
1073
+ *
1074
+ * A filter key that is not a promoted column lives inside the `extra` JSON
1075
+ * blob, so it is checked here in JS. It is applied rather than ignored — an
1076
+ * unapplied filter returns rows the caller will treat as matches.
1077
+ */
1078
+ function passesUnpushedFilter(row, filter) {
1079
+ if (filter === void 0) return true;
1080
+ for (const [key, expected] of Object.entries(filter.equals ?? {})) {
1081
+ if (isPromoted(key)) continue;
1082
+ if (row.extra[key] !== expected) return false;
1083
+ }
1084
+ for (const [key, [lo, hi]] of Object.entries(filter.between ?? {})) {
1085
+ if (isPromoted(key)) continue;
1086
+ const actual = row.extra[key];
1087
+ if (typeof actual !== "number" || actual < lo || actual > hi) return false;
1088
+ }
1089
+ return true;
1090
+ }
1091
+ function readRow(data) {
1092
+ const vector = data["vector"];
1093
+ if (typeof vector !== "string" || vector.length === 0) return null;
1094
+ const extra = data["extra"];
1095
+ return {
1096
+ vector,
1097
+ deviceId: numberOrNull(data["deviceId"]),
1098
+ timestamp: numberOrNull(data["timestamp"]),
1099
+ className: stringOrNull(data["className"]),
1100
+ modelId: stringOrNull(data["modelId"]),
1101
+ extra: isMetadata(extra) ? extra : {}
1102
+ };
1103
+ }
1104
+ function isMetadata(value) {
1105
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1106
+ }
1107
+ /** Narrow a stored metric string back to the enum. */
1108
+ function isMetric(value) {
1109
+ return value === "cosine" || value === "dot" || value === "euclidean";
1110
+ }
1111
+ /** Score two vectors under the index's metric. Higher is nearer in every case. */
1112
+ function scoreFor(metric, a, b) {
1113
+ if (metric === "cosine") return require_dist.cosineSimilarity(a, b);
1114
+ if (metric === "dot") {
1115
+ let sum = 0;
1116
+ for (let i = 0; i < a.length; i += 1) sum += (a[i] ?? 0) * (b[i] ?? 0);
1117
+ return sum;
1118
+ }
1119
+ let sum = 0;
1120
+ for (let i = 0; i < a.length; i += 1) {
1121
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
1122
+ sum += d * d;
1123
+ }
1124
+ const distance = Math.sqrt(sum);
1125
+ return distance === 0 ? 0 : -distance;
1126
+ }
1127
+ //#endregion
650
1128
  //#region src/builtins/sqlite-storage/sqlite-settings.addon.ts
651
1129
  /**
652
1130
  * SQLite Settings addon — the relational ENGINE behind the data door.
@@ -657,9 +1135,16 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
657
1135
  * ([D44](../../../../../docs/decisions/adr-0044.md)). Callers reach this
658
1136
  * engine only through the door — which is the point, because it is what
659
1137
  * makes the engine replaceable.
1138
+ *
1139
+ * It ALSO serves `vector-store` (singleton, internal), from the same database
1140
+ * file and the same open handle. That capability first shipped as its own addon
1141
+ * and the separation cost two round trips per write — caller → door → engine —
1142
+ * plus a runner process, to reach a database this addon already has open. The
1143
+ * capability is what keeps the backend swappable; the package never was.
660
1144
  */
661
1145
  var SqliteSettingsAddon = class extends require_dist.BaseAddon {
662
1146
  backend = null;
1147
+ vectorIndex = null;
663
1148
  constructor() {
664
1149
  super({});
665
1150
  }
@@ -683,10 +1168,36 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
683
1168
  this.ctx.logger.info("DB file status", { meta: { dbExists } });
684
1169
  this.backend = new SqliteSettingsBackend(dbPath, { ...require_dist.RUNTIME_DEFAULTS });
685
1170
  await this.backend.initialize();
1171
+ const vectorIndex = new SqliteVectorIndex({
1172
+ store: this.backend,
1173
+ logger: this.ctx.logger.child("VectorIndex")
1174
+ });
1175
+ await vectorIndex.loadIndexRegistry();
1176
+ this.vectorIndex = vectorIndex;
1177
+ const vectorProvider = {
1178
+ declareIndex: async (input) => {
1179
+ await vectorIndex.declareIndex(input.index, input.dim, input.metric);
1180
+ },
1181
+ upsert: async (input) => vectorIndex.upsert(input.index, input.items),
1182
+ query: async (input) => vectorIndex.query({
1183
+ index: input.index,
1184
+ vector: input.vector,
1185
+ topK: input.topK,
1186
+ ...input.minScore !== void 0 ? { minScore: input.minScore } : {},
1187
+ ...input.filter !== void 0 ? { filter: input.filter } : {}
1188
+ }),
1189
+ getByIds: async (input) => ({ items: await vectorIndex.getByIds(input.index, input.ids) }),
1190
+ deleteByIds: async (input) => ({ deleted: await vectorIndex.deleteByIds(input.index, input.ids) }),
1191
+ deleteByFilter: async (input) => ({ deleted: await vectorIndex.deleteByFilter(input.index, input.filter) }),
1192
+ stats: async (input) => vectorIndex.stats(input.index)
1193
+ };
686
1194
  this.ctx.logger.info("Initialized successfully");
687
1195
  return [{
688
1196
  capability: require_dist.dataStoreProviderCapability,
689
1197
  provider: this.backend
1198
+ }, {
1199
+ capability: require_dist.vectorStoreCapability,
1200
+ provider: vectorProvider
690
1201
  }];
691
1202
  }
692
1203
  async onShutdown() {
@@ -695,6 +1206,10 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
695
1206
  getBackend() {
696
1207
  return this.backend;
697
1208
  }
1209
+ /** Exposed for tests; null before `onInitialize`. */
1210
+ getVectorIndex() {
1211
+ return this.vectorIndex;
1212
+ }
698
1213
  };
699
1214
  //#endregion
700
1215
  exports.SqliteSettingsAddon = SqliteSettingsAddon;