agentfootprint 8.7.0 → 8.9.0

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 (245) hide show
  1. package/AGENTS.md +12 -4
  2. package/CLAUDE.md +7 -3
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/ai-instructions/setup.sh +0 -0
  5. package/bin/agentfootprint-lint-tools.mjs +0 -0
  6. package/dist/adapters/memory/sqliteVector.js +875 -0
  7. package/dist/adapters/memory/sqliteVector.js.map +1 -0
  8. package/dist/core/Agent.js +6 -0
  9. package/dist/core/Agent.js.map +1 -1
  10. package/dist/core/agent/buildAgentChart.js +12 -1
  11. package/dist/core/agent/buildAgentChart.js.map +1 -1
  12. package/dist/core/agent/buildDynamicAgentChart.js +12 -1
  13. package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
  14. package/dist/core/agent/memoryRecallInjections.js +100 -10
  15. package/dist/core/agent/memoryRecallInjections.js.map +1 -1
  16. package/dist/core/agent/stages/deliver.js.map +1 -1
  17. package/dist/core/slots/buildSystemPromptSlot.js +10 -0
  18. package/dist/core/slots/buildSystemPromptSlot.js.map +1 -1
  19. package/dist/core/slots/helpers.js +12 -10
  20. package/dist/core/slots/helpers.js.map +1 -1
  21. package/dist/embedders/index.js +11 -0
  22. package/dist/embedders/index.js.map +1 -1
  23. package/dist/esm/adapters/memory/sqliteVector.d.ts +251 -0
  24. package/dist/esm/adapters/memory/sqliteVector.js +869 -0
  25. package/dist/esm/adapters/memory/sqliteVector.js.map +1 -0
  26. package/dist/esm/core/Agent.js +6 -0
  27. package/dist/esm/core/Agent.js.map +1 -1
  28. package/dist/esm/core/agent/buildAgentChart.js +13 -2
  29. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  30. package/dist/esm/core/agent/buildDynamicAgentChart.js +13 -2
  31. package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
  32. package/dist/esm/core/agent/memoryRecallInjections.d.ts +13 -3
  33. package/dist/esm/core/agent/memoryRecallInjections.js +101 -11
  34. package/dist/esm/core/agent/memoryRecallInjections.js.map +1 -1
  35. package/dist/esm/core/agent/stages/deliver.d.ts +6 -3
  36. package/dist/esm/core/agent/stages/deliver.js.map +1 -1
  37. package/dist/esm/core/slots/buildSystemPromptSlot.js +10 -0
  38. package/dist/esm/core/slots/buildSystemPromptSlot.js.map +1 -1
  39. package/dist/esm/core/slots/helpers.d.ts +11 -2
  40. package/dist/esm/core/slots/helpers.js +11 -9
  41. package/dist/esm/core/slots/helpers.js.map +1 -1
  42. package/dist/esm/embedders/index.js +11 -0
  43. package/dist/esm/embedders/index.js.map +1 -1
  44. package/dist/esm/events/payloads.d.ts +63 -0
  45. package/dist/esm/events/registry.d.ts +3 -1
  46. package/dist/esm/events/registry.js +2 -0
  47. package/dist/esm/events/registry.js.map +1 -1
  48. package/dist/esm/hosting/sqliteSessions.d.ts +7 -13
  49. package/dist/esm/hosting/sqliteSessions.js +8 -25
  50. package/dist/esm/hosting/sqliteSessions.js.map +1 -1
  51. package/dist/esm/index.d.ts +2 -1
  52. package/dist/esm/index.js +6 -1
  53. package/dist/esm/index.js.map +1 -1
  54. package/dist/esm/lib/fnv1a.d.ts +16 -0
  55. package/dist/esm/lib/fnv1a.js +24 -0
  56. package/dist/esm/lib/fnv1a.js.map +1 -0
  57. package/dist/esm/lib/injection-engine/types.d.ts +17 -0
  58. package/dist/esm/lib/injection-engine/types.js.map +1 -1
  59. package/dist/esm/lib/rag/defineRAG.d.ts +126 -26
  60. package/dist/esm/lib/rag/defineRAG.js +112 -25
  61. package/dist/esm/lib/rag/defineRAG.js.map +1 -1
  62. package/dist/esm/lib/rag/index.d.ts +1 -1
  63. package/dist/esm/lib/rag/index.js +1 -1
  64. package/dist/esm/lib/rag/index.js.map +1 -1
  65. package/dist/esm/lib/rag/indexDocuments.d.ts +26 -4
  66. package/dist/esm/lib/rag/indexDocuments.js +12 -1
  67. package/dist/esm/lib/rag/indexDocuments.js.map +1 -1
  68. package/dist/esm/lib/sqliteUnavailable.d.ts +55 -0
  69. package/dist/esm/lib/sqliteUnavailable.js +61 -0
  70. package/dist/esm/lib/sqliteUnavailable.js.map +1 -0
  71. package/dist/esm/memory/define.js +38 -2
  72. package/dist/esm/memory/define.js.map +1 -1
  73. package/dist/esm/memory/define.types.d.ts +93 -1
  74. package/dist/esm/memory/define.types.js +18 -0
  75. package/dist/esm/memory/define.types.js.map +1 -1
  76. package/dist/esm/memory/embedding/embedMessages.js +16 -0
  77. package/dist/esm/memory/embedding/embedMessages.js.map +1 -1
  78. package/dist/esm/memory/embedding/emitEmbedding.d.ts +38 -0
  79. package/dist/esm/memory/embedding/emitEmbedding.js +15 -0
  80. package/dist/esm/memory/embedding/emitEmbedding.js.map +1 -0
  81. package/dist/esm/memory/embedding/loadRelevant.d.ts +49 -15
  82. package/dist/esm/memory/embedding/loadRelevant.js +150 -7
  83. package/dist/esm/memory/embedding/loadRelevant.js.map +1 -1
  84. package/dist/esm/memory/embedding/mockEmbedder.js +4 -0
  85. package/dist/esm/memory/embedding/mockEmbedder.js.map +1 -1
  86. package/dist/esm/memory/embedding/types.d.ts +24 -0
  87. package/dist/esm/memory/index.d.ts +2 -1
  88. package/dist/esm/memory/index.js +2 -1
  89. package/dist/esm/memory/index.js.map +1 -1
  90. package/dist/esm/memory/pipeline/semantic.d.ts +15 -0
  91. package/dist/esm/memory/pipeline/semantic.js +3 -1
  92. package/dist/esm/memory/pipeline/semantic.js.map +1 -1
  93. package/dist/esm/memory/retrieval/index.d.ts +10 -0
  94. package/dist/esm/memory/retrieval/index.js +3 -0
  95. package/dist/esm/memory/retrieval/index.js.map +1 -0
  96. package/dist/esm/memory/retrieval/provenance.d.ts +43 -0
  97. package/dist/esm/memory/retrieval/provenance.js +60 -0
  98. package/dist/esm/memory/retrieval/provenance.js.map +1 -0
  99. package/dist/esm/memory/retrieval/topK.d.ts +67 -0
  100. package/dist/esm/memory/retrieval/topK.js +53 -0
  101. package/dist/esm/memory/retrieval/topK.js.map +1 -0
  102. package/dist/esm/memory/retrieval/types.d.ts +189 -0
  103. package/dist/esm/memory/retrieval/types.js +2 -0
  104. package/dist/esm/memory/retrieval/types.js.map +1 -0
  105. package/dist/esm/memory/stages/formatDefault.d.ts +52 -26
  106. package/dist/esm/memory/stages/formatDefault.js +118 -28
  107. package/dist/esm/memory/stages/formatDefault.js.map +1 -1
  108. package/dist/esm/memory/stages/pickByBudget.js +53 -3
  109. package/dist/esm/memory/stages/pickByBudget.js.map +1 -1
  110. package/dist/esm/memory/stages/types.d.ts +15 -0
  111. package/dist/esm/memory/wire/mountMemoryPipeline.d.ts +25 -0
  112. package/dist/esm/memory/wire/mountMemoryPipeline.js +11 -2
  113. package/dist/esm/memory/wire/mountMemoryPipeline.js.map +1 -1
  114. package/dist/esm/memory-providers.d.ts +2 -0
  115. package/dist/esm/memory-providers.js +8 -0
  116. package/dist/esm/memory-providers.js.map +1 -1
  117. package/dist/esm/observe.d.ts +2 -0
  118. package/dist/esm/observe.js +2 -0
  119. package/dist/esm/observe.js.map +1 -1
  120. package/dist/esm/recorders/core/EmbeddingRecorder.d.ts +19 -0
  121. package/dist/esm/recorders/core/EmbeddingRecorder.js +24 -0
  122. package/dist/esm/recorders/core/EmbeddingRecorder.js.map +1 -0
  123. package/dist/events/registry.js +2 -0
  124. package/dist/events/registry.js.map +1 -1
  125. package/dist/hosting/sqliteSessions.js +10 -27
  126. package/dist/hosting/sqliteSessions.js.map +1 -1
  127. package/dist/index.js +8 -1
  128. package/dist/index.js.map +1 -1
  129. package/dist/lib/fnv1a.js +28 -0
  130. package/dist/lib/fnv1a.js.map +1 -0
  131. package/dist/lib/injection-engine/types.js.map +1 -1
  132. package/dist/lib/rag/defineRAG.js +113 -26
  133. package/dist/lib/rag/defineRAG.js.map +1 -1
  134. package/dist/lib/rag/index.js +2 -1
  135. package/dist/lib/rag/index.js.map +1 -1
  136. package/dist/lib/rag/indexDocuments.js +12 -1
  137. package/dist/lib/rag/indexDocuments.js.map +1 -1
  138. package/dist/lib/sqliteUnavailable.js +65 -0
  139. package/dist/lib/sqliteUnavailable.js.map +1 -0
  140. package/dist/memory/define.js +38 -2
  141. package/dist/memory/define.js.map +1 -1
  142. package/dist/memory/define.types.js +21 -1
  143. package/dist/memory/define.types.js.map +1 -1
  144. package/dist/memory/embedding/embedMessages.js +16 -0
  145. package/dist/memory/embedding/embedMessages.js.map +1 -1
  146. package/dist/memory/embedding/emitEmbedding.js +19 -0
  147. package/dist/memory/embedding/emitEmbedding.js.map +1 -0
  148. package/dist/memory/embedding/loadRelevant.js +152 -8
  149. package/dist/memory/embedding/loadRelevant.js.map +1 -1
  150. package/dist/memory/embedding/mockEmbedder.js +4 -0
  151. package/dist/memory/embedding/mockEmbedder.js.map +1 -1
  152. package/dist/memory/index.js +5 -1
  153. package/dist/memory/index.js.map +1 -1
  154. package/dist/memory/pipeline/semantic.js +2 -0
  155. package/dist/memory/pipeline/semantic.js.map +1 -1
  156. package/dist/memory/retrieval/index.js +9 -0
  157. package/dist/memory/retrieval/index.js.map +1 -0
  158. package/dist/memory/retrieval/provenance.js +65 -0
  159. package/dist/memory/retrieval/provenance.js.map +1 -0
  160. package/dist/memory/retrieval/topK.js +57 -0
  161. package/dist/memory/retrieval/topK.js.map +1 -0
  162. package/dist/memory/retrieval/types.js +3 -0
  163. package/dist/memory/retrieval/types.js.map +1 -0
  164. package/dist/memory/stages/formatDefault.js +118 -28
  165. package/dist/memory/stages/formatDefault.js.map +1 -1
  166. package/dist/memory/stages/pickByBudget.js +53 -3
  167. package/dist/memory/stages/pickByBudget.js.map +1 -1
  168. package/dist/memory/wire/mountMemoryPipeline.js +11 -2
  169. package/dist/memory/wire/mountMemoryPipeline.js.map +1 -1
  170. package/dist/memory-providers.js +13 -1
  171. package/dist/memory-providers.js.map +1 -1
  172. package/dist/observe.js +4 -1
  173. package/dist/observe.js.map +1 -1
  174. package/dist/recorders/core/EmbeddingRecorder.js +28 -0
  175. package/dist/recorders/core/EmbeddingRecorder.js.map +1 -0
  176. package/dist/types/adapters/memory/sqliteVector.d.ts +252 -0
  177. package/dist/types/adapters/memory/sqliteVector.d.ts.map +1 -0
  178. package/dist/types/core/Agent.d.ts.map +1 -1
  179. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  180. package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
  181. package/dist/types/core/agent/memoryRecallInjections.d.ts +13 -3
  182. package/dist/types/core/agent/memoryRecallInjections.d.ts.map +1 -1
  183. package/dist/types/core/agent/stages/deliver.d.ts +6 -3
  184. package/dist/types/core/agent/stages/deliver.d.ts.map +1 -1
  185. package/dist/types/core/slots/buildSystemPromptSlot.d.ts.map +1 -1
  186. package/dist/types/core/slots/helpers.d.ts +11 -2
  187. package/dist/types/core/slots/helpers.d.ts.map +1 -1
  188. package/dist/types/embedders/index.d.ts.map +1 -1
  189. package/dist/types/events/payloads.d.ts +63 -0
  190. package/dist/types/events/payloads.d.ts.map +1 -1
  191. package/dist/types/events/registry.d.ts +3 -1
  192. package/dist/types/events/registry.d.ts.map +1 -1
  193. package/dist/types/hosting/sqliteSessions.d.ts +7 -13
  194. package/dist/types/hosting/sqliteSessions.d.ts.map +1 -1
  195. package/dist/types/index.d.ts +2 -1
  196. package/dist/types/index.d.ts.map +1 -1
  197. package/dist/types/lib/fnv1a.d.ts +17 -0
  198. package/dist/types/lib/fnv1a.d.ts.map +1 -0
  199. package/dist/types/lib/injection-engine/types.d.ts +17 -0
  200. package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
  201. package/dist/types/lib/rag/defineRAG.d.ts +126 -26
  202. package/dist/types/lib/rag/defineRAG.d.ts.map +1 -1
  203. package/dist/types/lib/rag/index.d.ts +1 -1
  204. package/dist/types/lib/rag/index.d.ts.map +1 -1
  205. package/dist/types/lib/rag/indexDocuments.d.ts +26 -4
  206. package/dist/types/lib/rag/indexDocuments.d.ts.map +1 -1
  207. package/dist/types/lib/sqliteUnavailable.d.ts +56 -0
  208. package/dist/types/lib/sqliteUnavailable.d.ts.map +1 -0
  209. package/dist/types/memory/define.d.ts.map +1 -1
  210. package/dist/types/memory/define.types.d.ts +93 -1
  211. package/dist/types/memory/define.types.d.ts.map +1 -1
  212. package/dist/types/memory/embedding/embedMessages.d.ts.map +1 -1
  213. package/dist/types/memory/embedding/emitEmbedding.d.ts +39 -0
  214. package/dist/types/memory/embedding/emitEmbedding.d.ts.map +1 -0
  215. package/dist/types/memory/embedding/loadRelevant.d.ts +49 -15
  216. package/dist/types/memory/embedding/loadRelevant.d.ts.map +1 -1
  217. package/dist/types/memory/embedding/mockEmbedder.d.ts.map +1 -1
  218. package/dist/types/memory/embedding/types.d.ts +24 -0
  219. package/dist/types/memory/embedding/types.d.ts.map +1 -1
  220. package/dist/types/memory/index.d.ts +2 -1
  221. package/dist/types/memory/index.d.ts.map +1 -1
  222. package/dist/types/memory/pipeline/semantic.d.ts +15 -0
  223. package/dist/types/memory/pipeline/semantic.d.ts.map +1 -1
  224. package/dist/types/memory/retrieval/index.d.ts +11 -0
  225. package/dist/types/memory/retrieval/index.d.ts.map +1 -0
  226. package/dist/types/memory/retrieval/provenance.d.ts +44 -0
  227. package/dist/types/memory/retrieval/provenance.d.ts.map +1 -0
  228. package/dist/types/memory/retrieval/topK.d.ts +68 -0
  229. package/dist/types/memory/retrieval/topK.d.ts.map +1 -0
  230. package/dist/types/memory/retrieval/types.d.ts +190 -0
  231. package/dist/types/memory/retrieval/types.d.ts.map +1 -0
  232. package/dist/types/memory/stages/formatDefault.d.ts +52 -26
  233. package/dist/types/memory/stages/formatDefault.d.ts.map +1 -1
  234. package/dist/types/memory/stages/pickByBudget.d.ts.map +1 -1
  235. package/dist/types/memory/stages/types.d.ts +15 -0
  236. package/dist/types/memory/stages/types.d.ts.map +1 -1
  237. package/dist/types/memory/wire/mountMemoryPipeline.d.ts +25 -0
  238. package/dist/types/memory/wire/mountMemoryPipeline.d.ts.map +1 -1
  239. package/dist/types/memory-providers.d.ts +2 -0
  240. package/dist/types/memory-providers.d.ts.map +1 -1
  241. package/dist/types/observe.d.ts +2 -0
  242. package/dist/types/observe.d.ts.map +1 -1
  243. package/dist/types/recorders/core/EmbeddingRecorder.d.ts +20 -0
  244. package/dist/types/recorders/core/EmbeddingRecorder.d.ts.map +1 -0
  245. package/package.json +1 -1
@@ -0,0 +1,875 @@
1
+ "use strict";
2
+ /**
3
+ * adapters/memory/sqliteVector — a corpus in a file, so you embed it once.
4
+ *
5
+ * `InMemoryStore` is a `Map`, and its cost is the one nobody notices until the
6
+ * bill arrives: **restart the process and the whole corpus is re-embedded.**
7
+ * That is fine for three documents and absurd for ten thousand. The step up was
8
+ * "bring a vector database", and the step between those two — *one machine, one
9
+ * file, nothing to install* — was a store every consumer had to write for
10
+ * themselves. This is that store.
11
+ *
12
+ * ── The two-phase cost model this exists to fix ─────────────────────────────
13
+ * **Index time** embeds the corpus. It happens once, in its own process, and
14
+ * its cost scales with the size of your documents. **Query time** embeds one
15
+ * thing — the user's question — per retrieval, and its cost scales with
16
+ * traffic. A 10,000-chunk corpus is 10,000 embeddings *once* and one embedding
17
+ * per question thereafter. With a `Map` it is 10,000 embeddings *per restart*.
18
+ * That is the whole argument for a file.
19
+ *
20
+ * ── Exact search, and the ceiling said out loud ─────────────────────────────
21
+ * Vectors live in SQLite as `Float32Array` blobs. On the first search of a
22
+ * namespace they are hydrated into ONE resident `Float32Array` matrix,
23
+ * normalised, and every later query is an exact dot product across it. There is
24
+ * **no approximate index and no pretence of one**: this returns the true top-K,
25
+ * or it does not answer.
26
+ *
27
+ * Measured against THIS implementation on Node 22.16, Apple silicon, one
28
+ * namespace, median of five queries:
29
+ *
30
+ * | corpus | query | resident matrix | file | first search (hydration) |
31
+ * |---|---|---|---|---|
32
+ * | 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
33
+ * | 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
34
+ * | 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
35
+ * | 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
36
+ * | 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
37
+ *
38
+ * **The documented ceiling is 50,000 chunks.** Below it, every query is under
39
+ * 100 ms at every embedder this library ships and the resident matrix is under
40
+ * ~300 MB. It degrades linearly and predictably to about 100,000. Above that,
41
+ * or when the process cannot hold the matrix, move to a managed vector database
42
+ * — `MemoryStore` is the seam, and nothing else in your code changes. For scale
43
+ * intuition: 50,000 chunks at ~1,000 characters is roughly 50 MB of text, on
44
+ * the order of 25,000 pages.
45
+ *
46
+ * **Hydration is the number to plan around, not the query.** Steady-state
47
+ * search is fast everywhere in that table; reading the vectors off disk the
48
+ * FIRST time is what costs, and at 50,000 × 1536 it is 5.7 seconds. Paid
49
+ * lazily, that lands on whoever asks the first question after a deploy. Call
50
+ * {@link SqliteVectorStore.warm} at boot to pay it somewhere you chose — see
51
+ * that method. Smaller vectors are dramatically cheaper here: 384 dimensions
52
+ * hydrates 100,000 chunks in under a second, which is one more reason a
53
+ * 384-dimension embedder is the better default for a corpus this size.
54
+ *
55
+ * A loadable extension (sqlite-vec) was measured against this and deliberately
56
+ * NOT taken: it is roughly 2× faster at these sizes, and costs a native binary
57
+ * on a five-platform matrix (no musl, no Windows/arm64) plus a pre-1.0
58
+ * dependency. Two times, at sizes where we are already under 100 ms, does not
59
+ * buy that.
60
+ *
61
+ * ── One process on one machine ──────────────────────────────────────────────
62
+ * The same ceiling `sqliteSessions` states, for the same reason. It survives a
63
+ * restart, a crash, a deploy. It is NOT distributed. WAL gives one writer and
64
+ * many readers at once; a second writer waits up to `busyTimeoutMs` and then
65
+ * fails loudly rather than queueing forever.
66
+ *
67
+ * ── Zero dependencies, and the version floor that buys ──────────────────────
68
+ * SQLite is *inside Node* — no install, no native build, no peer dependency.
69
+ * The price is a version floor this package does not otherwise have, so the
70
+ * module is loaded when you actually construct a store and its absence is
71
+ * refused by name. There is deliberately **no fallback to memory**: a corpus
72
+ * that silently forgot every document on restart looks, from the outside,
73
+ * exactly like a corpus that was never built.
74
+ */
75
+ Object.defineProperty(exports, "__esModule", { value: true });
76
+ exports.sqliteVectorStore = exports.EmbedderMismatchError = exports.UnreadableIndexFileError = void 0;
77
+ const index_js_1 = require("../../memory/identity/index.js");
78
+ const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
79
+ const sqliteUnavailable_js_1 = require("../../lib/sqliteUnavailable.js");
80
+ // ─── The refusals ────────────────────────────────────────────────────
81
+ /**
82
+ * Raised when the file exists but this runtime cannot use it as a vector index.
83
+ *
84
+ * The law `sqliteSessions` states for a session file, one domain over: **an
85
+ * unreadable index and an empty one are different facts, and only one of them
86
+ * is safe to answer with "no matches".** A store that opened a corrupt file as
87
+ * an empty database would answer every question from the model's own weights
88
+ * and log nothing.
89
+ *
90
+ * `problem` is the fact to branch on:
91
+ *
92
+ * - `'cannot-open'` — not a SQLite database, or not readable.
93
+ * - `'not-our-schema'` — a database whose `af_vectors` table is somebody
94
+ * else's table of that name. Point the store at its own file.
95
+ * - `'newer-schema'` — written by a newer agentfootprint than this one.
96
+ */
97
+ class UnreadableIndexFileError extends Error {
98
+ code = 'ERR_UNREADABLE_INDEX_FILE';
99
+ /** The file that was refused. */
100
+ file;
101
+ /** Which of the three cases this is. */
102
+ problem;
103
+ constructor(file, problem, detail) {
104
+ super(`[memory] the vector index at '${file}' cannot be used: ${detail} ` +
105
+ `An unreadable index and an empty one are different facts, and only one of them ` +
106
+ `is safe to answer with "no matches" — so this refuses rather than quietly ` +
107
+ `answering every question from the model alone on top of a file that already ` +
108
+ `exists. ` +
109
+ (problem === 'newer-schema'
110
+ ? `An index written by a newer agentfootprint needs a runtime that knows that ` +
111
+ `schema; roll forward, or point this one at its own file.`
112
+ : problem === 'not-our-schema'
113
+ ? `Give the store a file of its own rather than sharing one with tables of ` +
114
+ `the same name.`
115
+ : `Check the path and its permissions, or move the file aside to start a ` + `new one.`));
116
+ this.name = 'UnreadableIndexFileError';
117
+ this.file = file;
118
+ this.problem = problem;
119
+ }
120
+ }
121
+ exports.UnreadableIndexFileError = UnreadableIndexFileError;
122
+ /**
123
+ * Raised when a vector meets an index built by a different embedder.
124
+ *
125
+ * **This is the refusal that keeps the store honest.** Cosine similarity between
126
+ * two different embedding spaces is not a weak signal — it is not a signal at
127
+ * all, and it comes back as a confident number in the same 0-to-1 range as a
128
+ * real one. There is no threshold that separates them, and nothing downstream
129
+ * can tell them apart. So the mismatch is refused where it happens, on both
130
+ * sides:
131
+ *
132
+ * - at **write**, so a second embedder's vectors never enter a namespace;
133
+ * - at **query**, so a swapped embedder never scores against the old ones.
134
+ *
135
+ * The named fix is an explicit re-index — delete the namespace and build it
136
+ * again with one embedder, or point the retriever at a different file. It is
137
+ * never a fallback: silently ignoring the mismatch is the failure, and silently
138
+ * re-embedding somebody's corpus is a bill they did not agree to.
139
+ *
140
+ * `problem` says which half is wrong. `'dimensions'` is arithmetically
141
+ * impossible to score at all; `'model'` would score, and lie.
142
+ */
143
+ class EmbedderMismatchError extends Error {
144
+ code = 'ERR_EMBEDDER_MISMATCH';
145
+ /** The fingerprint the namespace was built with, `'<id>@<dims>'`. */
146
+ indexed;
147
+ /** The fingerprint that just arrived. */
148
+ incoming;
149
+ /** Which half disagrees. */
150
+ problem;
151
+ constructor(namespace, indexed, incoming, problem, operation) {
152
+ super(`[memory] cannot ${operation} the namespace '${namespace}': it was indexed by ` +
153
+ `'${indexed}' and this vector is from '${incoming}'. ` +
154
+ (problem === 'dimensions'
155
+ ? `Vectors of different lengths cannot be compared at all. `
156
+ : `Cosine similarity between two embedding spaces is not a weak signal — it is ` +
157
+ `not a signal, and it comes back as a confident number in the same range as a ` +
158
+ `real one, which no threshold can separate. `) +
159
+ `Re-index this namespace with one embedder (delete it and build it again), or ` +
160
+ `point this store at a different file. This refuses rather than re-embedding ` +
161
+ `your corpus on your behalf — that is a bill you did not agree to — and rather ` +
162
+ `than mixing the two, which would silently corrupt every ranking it touched.`);
163
+ this.name = 'EmbedderMismatchError';
164
+ this.indexed = indexed;
165
+ this.incoming = incoming;
166
+ this.problem = problem;
167
+ }
168
+ }
169
+ exports.EmbedderMismatchError = EmbedderMismatchError;
170
+ // ─── The schema ──────────────────────────────────────────────────────
171
+ /**
172
+ * What this runtime writes. Bumped only for a change an older reader could not
173
+ * survive — and an older reader meeting a newer number refuses by name rather
174
+ * than reading what it half-understands.
175
+ */
176
+ const SCHEMA_VERSION = 1;
177
+ const VECTORS_TABLE = 'af_vectors';
178
+ const SIGNATURES_TABLE = 'af_signatures';
179
+ const FEEDBACK_TABLE = 'af_feedback';
180
+ const META_TABLE = 'af_index_meta';
181
+ /**
182
+ * `dims`, `embedder_fp` and `source_uri` are COLUMNS as well as facts inside the
183
+ * JSON, and the redundancy is deliberate: it lets the file answer "what is in
184
+ * this index, and who built it?" from the `sqlite3` command line during an
185
+ * incident, without a JSON parser and without this library. An index you cannot
186
+ * inspect with the tools already on the box is one you debug by guessing.
187
+ */
188
+ const VECTORS_COLUMNS = [
189
+ 'namespace',
190
+ 'id',
191
+ 'value',
192
+ 'metadata',
193
+ 'embedding',
194
+ 'dims',
195
+ 'embedder_fp',
196
+ 'version',
197
+ 'created_at',
198
+ 'updated_at',
199
+ 'last_accessed_at',
200
+ 'access_count',
201
+ 'ttl',
202
+ 'tier',
203
+ 'source',
204
+ 'source_uri',
205
+ ];
206
+ /**
207
+ * Open (or create) a vector index in one SQLite file.
208
+ *
209
+ * @throws SqliteUnavailableError when the running Node has no `node:sqlite`.
210
+ * @throws UnreadableIndexFileError when the file exists but cannot be used —
211
+ * never answered with an empty index.
212
+ * @throws EmbedderMismatchError from `put`/`putMany`/`search` when a vector
213
+ * meets a namespace built by a different embedder.
214
+ *
215
+ * @example Embed the corpus once, ever
216
+ * ```ts
217
+ * import { indexDocuments, defineRAG } from 'agentfootprint';
218
+ * import { sqliteVectorStore } from 'agentfootprint/memory';
219
+ * import { staticEmbedder } from 'agentfootprint/providers';
220
+ *
221
+ * const store = sqliteVectorStore({ file: './corpus.db' });
222
+ * const embedder = staticEmbedder();
223
+ *
224
+ * // First boot indexes; every boot after this one finds the vectors already there.
225
+ * await indexDocuments(store, embedder, docs, { embedderId: embedder.id });
226
+ *
227
+ * const agent = Agent.create({ provider })
228
+ * .rag(defineRAG({ id: 'docs', store, embedder, embedderId: embedder.id }))
229
+ * .build();
230
+ * ```
231
+ */
232
+ function sqliteVectorStore(options) {
233
+ const { file, busyTimeoutMs = 5000 } = options;
234
+ if (file === ':memory:' || file.trim() === '') {
235
+ throw new TypeError(`[memory] sqliteVectorStore({ file: '${file}' }) is not a durable index. ` +
236
+ `':memory:' looks like a file and keeps nothing across a restart, so every boot ` +
237
+ `would re-embed the whole corpus — the one cost this store exists to remove. ` +
238
+ `Use InMemoryStore when an in-process index is what you want — it says so in ` +
239
+ `its name — or give this one a real path.`);
240
+ }
241
+ const DatabaseSync = options._sqlite ?? loadSqlite();
242
+ ensureParentDirectory(file);
243
+ let db;
244
+ try {
245
+ db = new DatabaseSync(file);
246
+ }
247
+ catch (err) {
248
+ throw new UnreadableIndexFileError(file, 'cannot-open', `${describe(err)}.`);
249
+ }
250
+ let journalMode;
251
+ try {
252
+ journalMode = applyPragmas(db, busyTimeoutMs);
253
+ ensureSchema(db, file);
254
+ }
255
+ catch (err) {
256
+ db.close();
257
+ if (err instanceof UnreadableIndexFileError)
258
+ throw err;
259
+ throw new UnreadableIndexFileError(file, 'cannot-open', `${describe(err)}.`);
260
+ }
261
+ const stmt = prepareStatements(db);
262
+ /** Resident matrices, one per namespace, dropped on any write to that namespace. */
263
+ const warm = new Map();
264
+ /** Fingerprints, read once per namespace and then held. */
265
+ const fingerprints = new Map();
266
+ let closed = false;
267
+ const open = (verb) => {
268
+ if (closed) {
269
+ throw new Error(`[memory] the sqliteVectorStore at '${file}' is closed, so it cannot ${verb}. ` +
270
+ `close() is final by design — reopening the file behind you would hide a ` +
271
+ `shutdown-ordering bug rather than surface it. Build a new store if you need ` +
272
+ `one after closing this.`);
273
+ }
274
+ };
275
+ const readFingerprint = (ns) => {
276
+ const held = fingerprints.get(ns);
277
+ if (held !== undefined)
278
+ return held;
279
+ const row = stmt.readFp.get(`fp:${ns}`);
280
+ if (typeof row?.value !== 'string')
281
+ return undefined;
282
+ fingerprints.set(ns, row.value);
283
+ return row.value;
284
+ };
285
+ const recordFingerprint = (ns, fp) => {
286
+ stmt.writeFp.run(`fp:${ns}`, fp);
287
+ fingerprints.set(ns, fp);
288
+ };
289
+ /**
290
+ * Compare an arriving fingerprint against the namespace's, refuse a real
291
+ * conflict, and adopt the arriving one when it is the first or the more
292
+ * specific of the two.
293
+ */
294
+ const reconcileFingerprint = (ns, incoming, operation) => {
295
+ const storedText = readFingerprint(ns);
296
+ if (storedText === undefined) {
297
+ if (operation === 'write to')
298
+ recordFingerprint(ns, fingerprintText(incoming));
299
+ return;
300
+ }
301
+ const stored = parseFingerprint(storedText);
302
+ const conflict = fingerprintConflict(stored, incoming);
303
+ if (conflict !== null) {
304
+ throw new EmbedderMismatchError(ns, storedText, fingerprintText(incoming), conflict, operation);
305
+ }
306
+ // Compatible. If the index recorded an anonymous embedder and this vector
307
+ // names one, upgrade the record — a named fingerprint can refuse a future
308
+ // swap that an anonymous one has to let through.
309
+ if (operation === 'write to' && stored.id === undefined && incoming.id !== undefined) {
310
+ recordFingerprint(ns, fingerprintText(incoming));
311
+ }
312
+ };
313
+ const writeEntry = (ns, entry) => {
314
+ const vector = toFloat32(entry.embedding);
315
+ if (vector) {
316
+ reconcileFingerprint(ns, {
317
+ ...(entry.embeddingModel !== undefined && { id: entry.embeddingModel }),
318
+ dims: vector.length,
319
+ }, 'write to');
320
+ }
321
+ stmt.upsert.run(ns, entry.id, JSON.stringify(entry.value ?? null), entry.metadata === undefined ? null : JSON.stringify(entry.metadata), vector ? blobOf(vector) : null, vector ? vector.length : 0, vector
322
+ ? fingerprintText({
323
+ ...(entry.embeddingModel !== undefined && { id: entry.embeddingModel }),
324
+ dims: vector.length,
325
+ })
326
+ : null, entry.version, entry.createdAt, entry.updatedAt, entry.lastAccessedAt, entry.accessCount, entry.ttl ?? null, entry.tier ?? null, entry.source === undefined ? null : JSON.stringify(entry.source), readSourceUri(entry), entry.embeddingModel ?? null);
327
+ warm.delete(ns);
328
+ };
329
+ const hydrate = (ns) => {
330
+ const held = warm.get(ns);
331
+ if (held)
332
+ return held;
333
+ const rows = stmt.vectorsOf.all(ns);
334
+ const dims = rows.length > 0 ? rows[0]?.dims ?? 0 : 0;
335
+ const matrix = new Float32Array(rows.length * dims);
336
+ const ids = [];
337
+ const ttls = [];
338
+ const tiers = [];
339
+ const embedderIds = [];
340
+ const degenerate = new Set();
341
+ let row = 0;
342
+ for (const r of rows) {
343
+ // A namespace cannot hold two vector lengths — the fingerprint refusal
344
+ // above is what guarantees it — but a file edited by hand can, and a
345
+ // ragged matrix is worse than a skipped row.
346
+ if (r.dims !== dims)
347
+ continue;
348
+ const vec = readFloat32(r.embedding, dims);
349
+ let norm = 0;
350
+ for (let i = 0; i < dims; i++)
351
+ norm += (vec[i] ?? 0) * (vec[i] ?? 0);
352
+ norm = Math.sqrt(norm);
353
+ const offset = row * dims;
354
+ if (norm === 0) {
355
+ degenerate.add(row);
356
+ }
357
+ else {
358
+ // Normalised at hydration, so every query is a dot product. The BLOB on
359
+ // disk keeps the ORIGINAL vector, so `get`/`list` round-trip exactly
360
+ // what the caller wrote.
361
+ for (let i = 0; i < dims; i++)
362
+ matrix[offset + i] = (vec[i] ?? 0) / norm;
363
+ }
364
+ ids.push(r.id);
365
+ ttls.push(r.ttl ?? undefined);
366
+ tiers.push(r.tier ?? undefined);
367
+ embedderIds.push(r.embedding_model ?? undefined);
368
+ row += 1;
369
+ }
370
+ const built = { ids, matrix, dims, ttls, tiers, embedderIds, degenerate };
371
+ warm.set(ns, built);
372
+ return built;
373
+ };
374
+ const store = {
375
+ journalMode,
376
+ file,
377
+ fingerprintOf(identity) {
378
+ open('report a fingerprint');
379
+ return readFingerprint((0, index_js_1.identityNamespace)(identity));
380
+ },
381
+ // eslint-disable-next-line @typescript-eslint/require-await
382
+ async warm(identity) {
383
+ open('warm a namespace');
384
+ const startedAt = Date.now();
385
+ const warmed = hydrate((0, index_js_1.identityNamespace)(identity));
386
+ return { count: warmed.ids.length, durationMs: Date.now() - startedAt };
387
+ },
388
+ // eslint-disable-next-line @typescript-eslint/require-await
389
+ async get(identity, id) {
390
+ open('read an entry');
391
+ const ns = (0, index_js_1.identityNamespace)(identity);
392
+ const row = stmt.select.get(ns, id);
393
+ if (row === undefined)
394
+ return null;
395
+ if (isExpired(row.ttl))
396
+ return null;
397
+ // Decay signals, the same side effect the port documents for `get`. It
398
+ // does NOT invalidate the warm matrix: access counters are not vectors.
399
+ stmt.touch.run(Date.now(), ns, id);
400
+ return rowToEntry(row);
401
+ },
402
+ async put(identity, entry) {
403
+ await this.putMany(identity, [entry]);
404
+ },
405
+ // eslint-disable-next-line @typescript-eslint/require-await
406
+ async putMany(identity, entries) {
407
+ open('write entries');
408
+ // The port requires an empty batch to be a no-op — callers rely on it to
409
+ // skip a round-trip on a turn that produced nothing.
410
+ if (entries.length === 0)
411
+ return;
412
+ const ns = (0, index_js_1.identityNamespace)(identity);
413
+ // ONE transaction for the batch. The port says atomicity is not
414
+ // guaranteed across a batch and most callers are append-idempotent — but
415
+ // a HALF-INDEXED corpus is a specific kind of bad: retrieval keeps
416
+ // working and quietly cannot see the documents that did not land, which
417
+ // reads as "the model does not know that" rather than as a failure. A
418
+ // file can offer all-or-nothing cheaply, so it does.
419
+ db.exec('BEGIN');
420
+ try {
421
+ for (const entry of entries)
422
+ writeEntry(ns, entry);
423
+ db.exec('COMMIT');
424
+ }
425
+ catch (err) {
426
+ try {
427
+ db.exec('ROLLBACK');
428
+ }
429
+ catch {
430
+ /* The original failure is the one worth reporting. */
431
+ }
432
+ warm.delete(ns);
433
+ throw err;
434
+ }
435
+ },
436
+ // eslint-disable-next-line @typescript-eslint/require-await
437
+ async putIfVersion(identity, entry, expectedVersion) {
438
+ open('write an entry');
439
+ const ns = (0, index_js_1.identityNamespace)(identity);
440
+ // Read and write inside one transaction, so the check and the write
441
+ // cannot be separated by another writer — which is the entire point of a
442
+ // compare-and-set.
443
+ db.exec('BEGIN IMMEDIATE');
444
+ try {
445
+ const row = stmt.selectVersion.get(ns, entry.id);
446
+ const current = row?.version;
447
+ if (current === undefined) {
448
+ if (expectedVersion !== 0) {
449
+ db.exec('COMMIT');
450
+ return { applied: false };
451
+ }
452
+ }
453
+ else if (current !== expectedVersion) {
454
+ db.exec('COMMIT');
455
+ return { applied: false, currentVersion: current };
456
+ }
457
+ writeEntry(ns, entry);
458
+ db.exec('COMMIT');
459
+ return { applied: true };
460
+ }
461
+ catch (err) {
462
+ try {
463
+ db.exec('ROLLBACK');
464
+ }
465
+ catch {
466
+ /* The original failure is the one worth reporting. */
467
+ }
468
+ warm.delete(ns);
469
+ throw err;
470
+ }
471
+ },
472
+ // eslint-disable-next-line @typescript-eslint/require-await
473
+ async list(identity, listOptions) {
474
+ open('list entries');
475
+ const ns = (0, index_js_1.identityNamespace)(identity);
476
+ const limit = Math.max(1, Math.floor(listOptions?.limit ?? 100));
477
+ const after = listOptions?.cursor ?? '';
478
+ const rows = stmt.list.all(ns, after, limit + 1);
479
+ const tierFilter = listOptions?.tiers ? new Set(listOptions.tiers) : undefined;
480
+ const page = [];
481
+ let cursor;
482
+ for (const row of rows) {
483
+ if (page.length === limit) {
484
+ // The extra row only ever exists to prove there IS a next page.
485
+ cursor = page[page.length - 1]?.id;
486
+ break;
487
+ }
488
+ if (isExpired(row.ttl))
489
+ continue;
490
+ if (tierFilter && (row.tier === null || !tierFilter.has(row.tier)))
491
+ continue;
492
+ page.push(rowToEntry(row));
493
+ }
494
+ return { entries: page, ...(cursor !== undefined && { cursor }) };
495
+ },
496
+ // eslint-disable-next-line @typescript-eslint/require-await
497
+ async delete(identity, id) {
498
+ open('delete an entry');
499
+ const ns = (0, index_js_1.identityNamespace)(identity);
500
+ stmt.remove.run(ns, id);
501
+ warm.delete(ns);
502
+ },
503
+ // eslint-disable-next-line @typescript-eslint/require-await
504
+ async seen(identity, signature) {
505
+ open('check a signature');
506
+ return stmt.seen.get((0, index_js_1.identityNamespace)(identity), signature) !== undefined;
507
+ },
508
+ // eslint-disable-next-line @typescript-eslint/require-await
509
+ async recordSignature(identity, signature) {
510
+ open('record a signature');
511
+ stmt.addSignature.run((0, index_js_1.identityNamespace)(identity), signature);
512
+ },
513
+ // eslint-disable-next-line @typescript-eslint/require-await
514
+ async feedback(identity, id, usefulness) {
515
+ open('record feedback');
516
+ // Non-finite values poison the aggregate; the port says adapters must
517
+ // reject them, and clamp the rest.
518
+ if (!Number.isFinite(usefulness))
519
+ return;
520
+ const clamped = Math.max(-1, Math.min(1, usefulness));
521
+ stmt.addFeedback.run((0, index_js_1.identityNamespace)(identity), id, clamped);
522
+ },
523
+ // eslint-disable-next-line @typescript-eslint/require-await
524
+ async getFeedback(identity, id) {
525
+ open('read feedback');
526
+ const row = stmt.readFeedback.get((0, index_js_1.identityNamespace)(identity), id);
527
+ const count = row?.count ?? 0;
528
+ if (count === 0)
529
+ return null;
530
+ return { average: (row?.total ?? 0) / count, count };
531
+ },
532
+ // eslint-disable-next-line @typescript-eslint/require-await
533
+ async forget(identity) {
534
+ open('forget a namespace');
535
+ const ns = (0, index_js_1.identityNamespace)(identity);
536
+ db.exec('BEGIN');
537
+ try {
538
+ stmt.forgetVectors.run(ns);
539
+ stmt.forgetSignatures.run(ns);
540
+ stmt.forgetFeedback.run(ns);
541
+ stmt.forgetMeta.run(`fp:${ns}`);
542
+ db.exec('COMMIT');
543
+ }
544
+ catch (err) {
545
+ try {
546
+ db.exec('ROLLBACK');
547
+ }
548
+ catch {
549
+ /* The original failure is the one worth reporting. */
550
+ }
551
+ throw err;
552
+ }
553
+ warm.delete(ns);
554
+ fingerprints.delete(ns);
555
+ },
556
+ // eslint-disable-next-line @typescript-eslint/require-await
557
+ async search(identity, query, searchOptions) {
558
+ open('search');
559
+ const ns = (0, index_js_1.identityNamespace)(identity);
560
+ const k = Math.max(1, Math.floor(searchOptions?.k ?? 10));
561
+ // Refused BEFORE the scan, on both halves of the fingerprint. A swapped
562
+ // embedder must not score against the old vectors — the numbers come back
563
+ // in the same range as real ones and no threshold separates them.
564
+ reconcileFingerprint(ns, {
565
+ ...(searchOptions?.embedderId !== undefined && { id: searchOptions.embedderId }),
566
+ dims: query.length,
567
+ }, 'search');
568
+ const warmed = hydrate(ns);
569
+ if (warmed.ids.length === 0 || warmed.dims === 0)
570
+ return [];
571
+ if (warmed.dims !== query.length)
572
+ return [];
573
+ // Normalise the query once; the matrix rows already are. Cosine is then
574
+ // one dot product per row and nothing else.
575
+ let qNorm = 0;
576
+ for (const v of query)
577
+ qNorm += v * v;
578
+ qNorm = Math.sqrt(qNorm);
579
+ if (qNorm === 0)
580
+ return [];
581
+ const q = new Float32Array(query.length);
582
+ for (let i = 0; i < query.length; i++)
583
+ q[i] = (query[i] ?? 0) / qNorm;
584
+ const now = Date.now();
585
+ const tierFilter = searchOptions?.tiers ? new Set(searchOptions.tiers) : undefined;
586
+ const embedderId = searchOptions?.embedderId;
587
+ const minScore = searchOptions?.minScore;
588
+ const { dims, matrix, ids } = warmed;
589
+ const scored = [];
590
+ for (let row = 0; row < ids.length; row++) {
591
+ if (warmed.degenerate.has(row))
592
+ continue;
593
+ const ttl = warmed.ttls[row];
594
+ if (ttl !== undefined && ttl <= now)
595
+ continue;
596
+ if (tierFilter) {
597
+ const tier = warmed.tiers[row];
598
+ if (tier === undefined || !tierFilter.has(tier))
599
+ continue;
600
+ }
601
+ if (embedderId !== undefined) {
602
+ const rowEmbedder = warmed.embedderIds[row];
603
+ if (rowEmbedder !== undefined && rowEmbedder !== embedderId)
604
+ continue;
605
+ }
606
+ const offset = row * dims;
607
+ let dot = 0;
608
+ for (let i = 0; i < dims; i++)
609
+ dot += (matrix[offset + i] ?? 0) * (q[i] ?? 0);
610
+ if (minScore !== undefined && dot < minScore)
611
+ continue;
612
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
613
+ scored.push({ id: ids[row], score: dot });
614
+ }
615
+ scored.sort((a, b) => {
616
+ if (b.score !== a.score)
617
+ return b.score - a.score;
618
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
619
+ });
620
+ // Hydrate only the winners' payloads. The matrix holds vectors, not
621
+ // documents — a 50,000-chunk namespace has no business materialising
622
+ // 50,000 JSON values to answer a top-3.
623
+ const out = [];
624
+ for (const hit of scored.slice(0, k)) {
625
+ const row = stmt.select.get(ns, hit.id);
626
+ if (row === undefined)
627
+ continue;
628
+ out.push({ entry: rowToEntry(row), score: hit.score });
629
+ }
630
+ return out;
631
+ },
632
+ close() {
633
+ if (closed)
634
+ return;
635
+ closed = true;
636
+ warm.clear();
637
+ db.close();
638
+ },
639
+ };
640
+ return store;
641
+ }
642
+ exports.sqliteVectorStore = sqliteVectorStore;
643
+ /** `'<id>@<dims>'`, with `'?'` for an embedder that did not name itself. */
644
+ function fingerprintText(fp) {
645
+ return `${fp.id ?? '?'}@${fp.dims}`;
646
+ }
647
+ function parseFingerprint(text) {
648
+ const at = text.lastIndexOf('@');
649
+ const id = at === -1 ? '?' : text.slice(0, at);
650
+ const dims = at === -1 ? 0 : Number(text.slice(at + 1));
651
+ return {
652
+ ...(id !== '?' && id !== '' && { id }),
653
+ dims: Number.isFinite(dims) ? dims : 0,
654
+ };
655
+ }
656
+ /**
657
+ * What, if anything, makes these two incompatible.
658
+ *
659
+ * Dimensions always decide: two lengths cannot be compared at all. Model ids
660
+ * decide only when BOTH sides named themselves — an anonymous vector is not
661
+ * evidence of a different embedder, and refusing on absence would break every
662
+ * caller who never passed an `embedderId`, which is most of them.
663
+ */
664
+ function fingerprintConflict(stored, incoming) {
665
+ if (stored.dims !== incoming.dims)
666
+ return 'dimensions';
667
+ if (stored.id !== undefined && incoming.id !== undefined && stored.id !== incoming.id) {
668
+ return 'model';
669
+ }
670
+ return null;
671
+ }
672
+ function rowToEntry(row) {
673
+ return {
674
+ id: row.id,
675
+ value: parseJson(row.value),
676
+ ...(row.metadata !== null && {
677
+ metadata: parseJson(row.metadata),
678
+ }),
679
+ version: row.version,
680
+ createdAt: row.created_at,
681
+ updatedAt: row.updated_at,
682
+ lastAccessedAt: row.last_accessed_at,
683
+ accessCount: row.access_count,
684
+ ...(row.ttl !== null && { ttl: row.ttl }),
685
+ ...(row.tier !== null && { tier: row.tier }),
686
+ ...(row.source !== null && {
687
+ source: parseJson(row.source),
688
+ }),
689
+ ...(row.embedding !== null &&
690
+ row.dims > 0 && { embedding: Array.from(readFloat32(row.embedding, row.dims)) }),
691
+ ...(row.embedding_model !== null && { embeddingModel: row.embedding_model }),
692
+ };
693
+ }
694
+ function parseJson(text) {
695
+ try {
696
+ return JSON.parse(text);
697
+ }
698
+ catch {
699
+ // A value this store could not read back. It was written as JSON by this
700
+ // store, so reaching here means the file was edited by something else.
701
+ return null;
702
+ }
703
+ }
704
+ function isExpired(ttl) {
705
+ return ttl !== null && ttl <= Date.now();
706
+ }
707
+ /** The document a chunk came from, lifted to a column so `sqlite3` can group by it. */
708
+ function readSourceUri(entry) {
709
+ const value = entry.value;
710
+ const meta = (value?.metadata ?? entry.metadata);
711
+ const uri = meta?.['docUri'] ?? meta?.['source'];
712
+ return typeof uri === 'string' && uri.length > 0 ? uri : null;
713
+ }
714
+ // ─── Vector encoding ─────────────────────────────────────────────────
715
+ function toFloat32(embedding) {
716
+ if (!embedding || embedding.length === 0)
717
+ return undefined;
718
+ const out = new Float32Array(embedding.length);
719
+ for (let i = 0; i < embedding.length; i++)
720
+ out[i] = embedding[i] ?? 0;
721
+ return out;
722
+ }
723
+ /** Little-endian `Float32Array` bytes — the platform's own layout, no conversion. */
724
+ function blobOf(vector) {
725
+ return new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength);
726
+ }
727
+ function readFloat32(blob, dims) {
728
+ // A BLOB comes back with its own byteOffset; a Float32Array view needs a
729
+ // 4-byte-aligned one, which is not guaranteed, so copy when it is not.
730
+ if (blob.byteOffset % 4 === 0) {
731
+ return new Float32Array(blob.buffer, blob.byteOffset, Math.min(dims, blob.byteLength / 4));
732
+ }
733
+ const copy = new Uint8Array(blob);
734
+ return new Float32Array(copy.buffer, 0, Math.min(dims, copy.byteLength / 4));
735
+ }
736
+ // ─── Internals ───────────────────────────────────────────────────────
737
+ function loadSqlite() {
738
+ try {
739
+ const mod = (0, lazyRequire_js_1.lazyRequire)('node:sqlite');
740
+ if (typeof mod?.DatabaseSync !== 'function') {
741
+ throw new Error('the module loaded but has no DatabaseSync');
742
+ }
743
+ return mod.DatabaseSync;
744
+ }
745
+ catch (err) {
746
+ throw new sqliteUnavailable_js_1.SqliteUnavailableError(nodeVersion(), describe(err), {
747
+ door: 'memory',
748
+ factory: 'sqliteVectorStore()',
749
+ alternative: 'InMemoryStore — which keeps the index in a Map and re-embeds the whole corpus on every restart, and says so in its name',
750
+ whyNotFallback: 'an index that silently forgot every document on restart looks, from the outside, exactly like a corpus that was never built',
751
+ });
752
+ }
753
+ }
754
+ function nodeVersion() {
755
+ return typeof process !== 'undefined' && typeof process.version === 'string'
756
+ ? process.version
757
+ : 'unknown';
758
+ }
759
+ function ensureParentDirectory(file) {
760
+ try {
761
+ const { mkdirSync } = (0, lazyRequire_js_1.lazyRequire)('node:fs');
762
+ const { dirname } = (0, lazyRequire_js_1.lazyRequire)('node:path');
763
+ mkdirSync(dirname(file), { recursive: true });
764
+ }
765
+ catch {
766
+ /* Reported by the open below, with the path and the real reason. */
767
+ }
768
+ }
769
+ /**
770
+ * Set the file up for concurrent use and report what it actually got.
771
+ *
772
+ * `journal_mode` runs first on purpose: it is the point at which SQLite reads
773
+ * the file header, so "this is not a database" surfaces here, at construction,
774
+ * rather than on the first query of the night.
775
+ */
776
+ function applyPragmas(db, busyTimeoutMs) {
777
+ const row = db.prepare('PRAGMA journal_mode = WAL').get();
778
+ // A pragma takes a literal, not a bound parameter, so the number is coerced
779
+ // to one before it reaches the statement rather than trusted to be one.
780
+ const waitMs = Number.isFinite(busyTimeoutMs) ? Math.max(0, Math.floor(busyTimeoutMs)) : 5000;
781
+ db.exec(`PRAGMA busy_timeout = ${waitMs}`);
782
+ // NORMAL, not FULL: with WAL that is durable across a process crash — which
783
+ // is what this store promises — without a disk sync per write. A power cut
784
+ // can still cost the most recent commits; the answer to that is a
785
+ // fleet-grade store, not a pragma.
786
+ db.exec('PRAGMA synchronous = NORMAL');
787
+ return typeof row?.journal_mode === 'string' ? row.journal_mode : 'unknown';
788
+ }
789
+ function ensureSchema(db, file) {
790
+ db.exec(`CREATE TABLE IF NOT EXISTS ${VECTORS_TABLE} (` +
791
+ `namespace TEXT NOT NULL, id TEXT NOT NULL, value TEXT NOT NULL, metadata TEXT, ` +
792
+ `embedding BLOB, dims INTEGER NOT NULL, embedder_fp TEXT, version INTEGER NOT NULL, ` +
793
+ `created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, ` +
794
+ `last_accessed_at INTEGER NOT NULL, access_count INTEGER NOT NULL, ` +
795
+ `ttl INTEGER, tier TEXT, source TEXT, source_uri TEXT, embedding_model TEXT, ` +
796
+ `PRIMARY KEY (namespace, id)) STRICT`);
797
+ // The identity check runs HERE — after the table, before anything that
798
+ // depends on its columns. `CREATE TABLE IF NOT EXISTS` succeeds against
799
+ // somebody else's table of the same name, and the very next statement
800
+ // (an index over `namespace`) would then fail on a missing column and be
801
+ // reported as 'cannot-open' — the right refusal for the wrong reason,
802
+ // telling the reader to check permissions on a file whose real problem is
803
+ // that it belongs to something else.
804
+ const columns = db.prepare(`PRAGMA table_info('${VECTORS_TABLE}')`).all().map((c) => String(c.name));
805
+ const missing = VECTORS_COLUMNS.filter((name) => !columns.includes(name));
806
+ if (missing.length > 0) {
807
+ throw new UnreadableIndexFileError(file, 'not-our-schema', `it has an '${VECTORS_TABLE}' table that is not this store's ` +
808
+ `(missing ${missing.join(', ')}; found ${columns.join(', ') || 'nothing'}).`);
809
+ }
810
+ db.exec(`CREATE INDEX IF NOT EXISTS ${VECTORS_TABLE}_ns ON ${VECTORS_TABLE}(namespace)`);
811
+ db.exec(`CREATE INDEX IF NOT EXISTS ${VECTORS_TABLE}_doc ON ${VECTORS_TABLE}(namespace, source_uri)`);
812
+ db.exec(`CREATE TABLE IF NOT EXISTS ${SIGNATURES_TABLE} (` +
813
+ `namespace TEXT NOT NULL, signature TEXT NOT NULL, PRIMARY KEY (namespace, signature)) STRICT`);
814
+ db.exec(`CREATE TABLE IF NOT EXISTS ${FEEDBACK_TABLE} (` +
815
+ `namespace TEXT NOT NULL, id TEXT NOT NULL, total REAL NOT NULL, count INTEGER NOT NULL, ` +
816
+ `PRIMARY KEY (namespace, id)) STRICT`);
817
+ db.exec(`CREATE TABLE IF NOT EXISTS ${META_TABLE} (key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT`);
818
+ const stored = db
819
+ .prepare(`SELECT value FROM ${META_TABLE} WHERE key = 'schema_version'`)
820
+ .get();
821
+ if (stored === undefined) {
822
+ db.prepare(`INSERT INTO ${META_TABLE} (key, value) VALUES ('schema_version', ?)`).run(String(SCHEMA_VERSION));
823
+ return;
824
+ }
825
+ const found = Number(stored.value);
826
+ if (!Number.isFinite(found) || found > SCHEMA_VERSION) {
827
+ throw new UnreadableIndexFileError(file, 'newer-schema', `it was written with schema version ${String(stored.value)} and this runtime ` +
828
+ `reads version ${SCHEMA_VERSION}.`);
829
+ }
830
+ }
831
+ function prepareStatements(db) {
832
+ const ENTRY_COLUMNS = 'id, value, metadata, embedding, dims, version, created_at, updated_at, ' +
833
+ 'last_accessed_at, access_count, ttl, tier, source, embedding_model';
834
+ return {
835
+ upsert: db.prepare(`INSERT INTO ${VECTORS_TABLE} (namespace, id, value, metadata, embedding, dims, ` +
836
+ `embedder_fp, version, created_at, updated_at, last_accessed_at, access_count, ` +
837
+ `ttl, tier, source, source_uri, embedding_model) ` +
838
+ `VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` +
839
+ `ON CONFLICT(namespace, id) DO UPDATE SET value = excluded.value, ` +
840
+ `metadata = excluded.metadata, embedding = excluded.embedding, dims = excluded.dims, ` +
841
+ `embedder_fp = excluded.embedder_fp, version = excluded.version, ` +
842
+ `created_at = excluded.created_at, updated_at = excluded.updated_at, ` +
843
+ `last_accessed_at = excluded.last_accessed_at, access_count = excluded.access_count, ` +
844
+ `ttl = excluded.ttl, tier = excluded.tier, source = excluded.source, ` +
845
+ `source_uri = excluded.source_uri, embedding_model = excluded.embedding_model`),
846
+ select: db.prepare(`SELECT ${ENTRY_COLUMNS} FROM ${VECTORS_TABLE} WHERE namespace = ? AND id = ?`),
847
+ selectVersion: db.prepare(`SELECT version FROM ${VECTORS_TABLE} WHERE namespace = ? AND id = ?`),
848
+ list: db.prepare(`SELECT ${ENTRY_COLUMNS} FROM ${VECTORS_TABLE} WHERE namespace = ? AND id > ? ` +
849
+ `ORDER BY id LIMIT ?`),
850
+ vectorsOf: db.prepare(`SELECT id, embedding, dims, ttl, tier, embedding_model FROM ${VECTORS_TABLE} ` +
851
+ `WHERE namespace = ? AND embedding IS NOT NULL ORDER BY id`),
852
+ touch: db.prepare(`UPDATE ${VECTORS_TABLE} SET last_accessed_at = ?, access_count = access_count + 1 ` +
853
+ `WHERE namespace = ? AND id = ?`),
854
+ remove: db.prepare(`DELETE FROM ${VECTORS_TABLE} WHERE namespace = ? AND id = ?`),
855
+ seen: db.prepare(`SELECT 1 FROM ${SIGNATURES_TABLE} WHERE namespace = ? AND signature = ?`),
856
+ addSignature: db.prepare(`INSERT INTO ${SIGNATURES_TABLE} (namespace, signature) VALUES (?, ?) ` +
857
+ `ON CONFLICT(namespace, signature) DO NOTHING`),
858
+ addFeedback: db.prepare(`INSERT INTO ${FEEDBACK_TABLE} (namespace, id, total, count) VALUES (?, ?, ?, 1) ` +
859
+ `ON CONFLICT(namespace, id) DO UPDATE SET total = total + excluded.total, ` +
860
+ `count = count + 1`),
861
+ readFeedback: db.prepare(`SELECT total, count FROM ${FEEDBACK_TABLE} WHERE namespace = ? AND id = ?`),
862
+ forgetVectors: db.prepare(`DELETE FROM ${VECTORS_TABLE} WHERE namespace = ?`),
863
+ forgetSignatures: db.prepare(`DELETE FROM ${SIGNATURES_TABLE} WHERE namespace = ?`),
864
+ forgetFeedback: db.prepare(`DELETE FROM ${FEEDBACK_TABLE} WHERE namespace = ?`),
865
+ forgetMeta: db.prepare(`DELETE FROM ${META_TABLE} WHERE key = ?`),
866
+ readFp: db.prepare(`SELECT value FROM ${META_TABLE} WHERE key = ?`),
867
+ writeFp: db.prepare(`INSERT INTO ${META_TABLE} (key, value) VALUES (?, ?) ` +
868
+ `ON CONFLICT(key) DO UPDATE SET value = excluded.value`),
869
+ };
870
+ }
871
+ /** One sentence about a thrown thing, for a message that has to stay readable. */
872
+ function describe(err) {
873
+ return err instanceof Error ? err.message : String(err);
874
+ }
875
+ //# sourceMappingURL=sqliteVector.js.map