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