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