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.
- package/AGENTS.md +12 -4
- package/CLAUDE.md +7 -3
- package/ai-instructions/claude-code/SKILL.md +1 -1
- package/ai-instructions/setup.sh +0 -0
- package/bin/agentfootprint-lint-tools.mjs +0 -0
- package/dist/adapters/memory/sqliteVector.js +875 -0
- package/dist/adapters/memory/sqliteVector.js.map +1 -0
- package/dist/core/Agent.js +6 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/agent/buildAgentChart.js +12 -1
- package/dist/core/agent/buildAgentChart.js.map +1 -1
- package/dist/core/agent/buildDynamicAgentChart.js +12 -1
- package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
- package/dist/core/agent/memoryRecallInjections.js +100 -10
- package/dist/core/agent/memoryRecallInjections.js.map +1 -1
- package/dist/core/agent/stages/deliver.js.map +1 -1
- package/dist/core/slots/buildSystemPromptSlot.js +10 -0
- package/dist/core/slots/buildSystemPromptSlot.js.map +1 -1
- package/dist/core/slots/helpers.js +12 -10
- package/dist/core/slots/helpers.js.map +1 -1
- package/dist/embedders/index.js +11 -0
- package/dist/embedders/index.js.map +1 -1
- package/dist/esm/adapters/memory/sqliteVector.d.ts +251 -0
- package/dist/esm/adapters/memory/sqliteVector.js +869 -0
- package/dist/esm/adapters/memory/sqliteVector.js.map +1 -0
- package/dist/esm/core/Agent.js +6 -0
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/agent/buildAgentChart.js +13 -2
- package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
- package/dist/esm/core/agent/buildDynamicAgentChart.js +13 -2
- package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
- package/dist/esm/core/agent/memoryRecallInjections.d.ts +13 -3
- package/dist/esm/core/agent/memoryRecallInjections.js +101 -11
- package/dist/esm/core/agent/memoryRecallInjections.js.map +1 -1
- package/dist/esm/core/agent/stages/deliver.d.ts +6 -3
- package/dist/esm/core/agent/stages/deliver.js.map +1 -1
- package/dist/esm/core/slots/buildSystemPromptSlot.js +10 -0
- package/dist/esm/core/slots/buildSystemPromptSlot.js.map +1 -1
- package/dist/esm/core/slots/helpers.d.ts +11 -2
- package/dist/esm/core/slots/helpers.js +11 -9
- package/dist/esm/core/slots/helpers.js.map +1 -1
- package/dist/esm/embedders/index.js +11 -0
- package/dist/esm/embedders/index.js.map +1 -1
- package/dist/esm/events/payloads.d.ts +63 -0
- package/dist/esm/events/registry.d.ts +3 -1
- package/dist/esm/events/registry.js +2 -0
- package/dist/esm/events/registry.js.map +1 -1
- package/dist/esm/hosting/sqliteSessions.d.ts +7 -13
- package/dist/esm/hosting/sqliteSessions.js +8 -25
- package/dist/esm/hosting/sqliteSessions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +6 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/fnv1a.d.ts +16 -0
- package/dist/esm/lib/fnv1a.js +24 -0
- package/dist/esm/lib/fnv1a.js.map +1 -0
- package/dist/esm/lib/injection-engine/types.d.ts +17 -0
- package/dist/esm/lib/injection-engine/types.js.map +1 -1
- package/dist/esm/lib/rag/defineRAG.d.ts +126 -26
- package/dist/esm/lib/rag/defineRAG.js +112 -25
- package/dist/esm/lib/rag/defineRAG.js.map +1 -1
- package/dist/esm/lib/rag/index.d.ts +1 -1
- package/dist/esm/lib/rag/index.js +1 -1
- package/dist/esm/lib/rag/index.js.map +1 -1
- package/dist/esm/lib/rag/indexDocuments.d.ts +26 -4
- package/dist/esm/lib/rag/indexDocuments.js +12 -1
- package/dist/esm/lib/rag/indexDocuments.js.map +1 -1
- package/dist/esm/lib/sqliteUnavailable.d.ts +55 -0
- package/dist/esm/lib/sqliteUnavailable.js +61 -0
- package/dist/esm/lib/sqliteUnavailable.js.map +1 -0
- package/dist/esm/memory/define.js +38 -2
- package/dist/esm/memory/define.js.map +1 -1
- package/dist/esm/memory/define.types.d.ts +93 -1
- package/dist/esm/memory/define.types.js +18 -0
- package/dist/esm/memory/define.types.js.map +1 -1
- package/dist/esm/memory/embedding/embedMessages.js +16 -0
- package/dist/esm/memory/embedding/embedMessages.js.map +1 -1
- package/dist/esm/memory/embedding/emitEmbedding.d.ts +38 -0
- package/dist/esm/memory/embedding/emitEmbedding.js +15 -0
- package/dist/esm/memory/embedding/emitEmbedding.js.map +1 -0
- package/dist/esm/memory/embedding/loadRelevant.d.ts +49 -15
- package/dist/esm/memory/embedding/loadRelevant.js +150 -7
- package/dist/esm/memory/embedding/loadRelevant.js.map +1 -1
- package/dist/esm/memory/embedding/mockEmbedder.js +4 -0
- package/dist/esm/memory/embedding/mockEmbedder.js.map +1 -1
- package/dist/esm/memory/embedding/types.d.ts +24 -0
- package/dist/esm/memory/index.d.ts +2 -1
- package/dist/esm/memory/index.js +2 -1
- package/dist/esm/memory/index.js.map +1 -1
- package/dist/esm/memory/pipeline/semantic.d.ts +15 -0
- package/dist/esm/memory/pipeline/semantic.js +3 -1
- package/dist/esm/memory/pipeline/semantic.js.map +1 -1
- package/dist/esm/memory/retrieval/index.d.ts +10 -0
- package/dist/esm/memory/retrieval/index.js +3 -0
- package/dist/esm/memory/retrieval/index.js.map +1 -0
- package/dist/esm/memory/retrieval/provenance.d.ts +43 -0
- package/dist/esm/memory/retrieval/provenance.js +60 -0
- package/dist/esm/memory/retrieval/provenance.js.map +1 -0
- package/dist/esm/memory/retrieval/topK.d.ts +67 -0
- package/dist/esm/memory/retrieval/topK.js +53 -0
- package/dist/esm/memory/retrieval/topK.js.map +1 -0
- package/dist/esm/memory/retrieval/types.d.ts +189 -0
- package/dist/esm/memory/retrieval/types.js +2 -0
- package/dist/esm/memory/retrieval/types.js.map +1 -0
- package/dist/esm/memory/stages/formatDefault.d.ts +52 -26
- package/dist/esm/memory/stages/formatDefault.js +118 -28
- package/dist/esm/memory/stages/formatDefault.js.map +1 -1
- package/dist/esm/memory/stages/pickByBudget.js +53 -3
- package/dist/esm/memory/stages/pickByBudget.js.map +1 -1
- package/dist/esm/memory/stages/types.d.ts +15 -0
- package/dist/esm/memory/wire/mountMemoryPipeline.d.ts +25 -0
- package/dist/esm/memory/wire/mountMemoryPipeline.js +11 -2
- package/dist/esm/memory/wire/mountMemoryPipeline.js.map +1 -1
- package/dist/esm/memory-providers.d.ts +2 -0
- package/dist/esm/memory-providers.js +8 -0
- package/dist/esm/memory-providers.js.map +1 -1
- package/dist/esm/observe.d.ts +2 -0
- package/dist/esm/observe.js +2 -0
- package/dist/esm/observe.js.map +1 -1
- package/dist/esm/recorders/core/EmbeddingRecorder.d.ts +19 -0
- package/dist/esm/recorders/core/EmbeddingRecorder.js +24 -0
- package/dist/esm/recorders/core/EmbeddingRecorder.js.map +1 -0
- package/dist/events/registry.js +2 -0
- package/dist/events/registry.js.map +1 -1
- package/dist/hosting/sqliteSessions.js +10 -27
- package/dist/hosting/sqliteSessions.js.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/fnv1a.js +28 -0
- package/dist/lib/fnv1a.js.map +1 -0
- package/dist/lib/injection-engine/types.js.map +1 -1
- package/dist/lib/rag/defineRAG.js +113 -26
- package/dist/lib/rag/defineRAG.js.map +1 -1
- package/dist/lib/rag/index.js +2 -1
- package/dist/lib/rag/index.js.map +1 -1
- package/dist/lib/rag/indexDocuments.js +12 -1
- package/dist/lib/rag/indexDocuments.js.map +1 -1
- package/dist/lib/sqliteUnavailable.js +65 -0
- package/dist/lib/sqliteUnavailable.js.map +1 -0
- package/dist/memory/define.js +38 -2
- package/dist/memory/define.js.map +1 -1
- package/dist/memory/define.types.js +21 -1
- package/dist/memory/define.types.js.map +1 -1
- package/dist/memory/embedding/embedMessages.js +16 -0
- package/dist/memory/embedding/embedMessages.js.map +1 -1
- package/dist/memory/embedding/emitEmbedding.js +19 -0
- package/dist/memory/embedding/emitEmbedding.js.map +1 -0
- package/dist/memory/embedding/loadRelevant.js +152 -8
- package/dist/memory/embedding/loadRelevant.js.map +1 -1
- package/dist/memory/embedding/mockEmbedder.js +4 -0
- package/dist/memory/embedding/mockEmbedder.js.map +1 -1
- package/dist/memory/index.js +5 -1
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/pipeline/semantic.js +2 -0
- package/dist/memory/pipeline/semantic.js.map +1 -1
- package/dist/memory/retrieval/index.js +9 -0
- package/dist/memory/retrieval/index.js.map +1 -0
- package/dist/memory/retrieval/provenance.js +65 -0
- package/dist/memory/retrieval/provenance.js.map +1 -0
- package/dist/memory/retrieval/topK.js +57 -0
- package/dist/memory/retrieval/topK.js.map +1 -0
- package/dist/memory/retrieval/types.js +3 -0
- package/dist/memory/retrieval/types.js.map +1 -0
- package/dist/memory/stages/formatDefault.js +118 -28
- package/dist/memory/stages/formatDefault.js.map +1 -1
- package/dist/memory/stages/pickByBudget.js +53 -3
- package/dist/memory/stages/pickByBudget.js.map +1 -1
- package/dist/memory/wire/mountMemoryPipeline.js +11 -2
- package/dist/memory/wire/mountMemoryPipeline.js.map +1 -1
- package/dist/memory-providers.js +13 -1
- package/dist/memory-providers.js.map +1 -1
- package/dist/observe.js +4 -1
- package/dist/observe.js.map +1 -1
- package/dist/recorders/core/EmbeddingRecorder.js +28 -0
- package/dist/recorders/core/EmbeddingRecorder.js.map +1 -0
- package/dist/types/adapters/memory/sqliteVector.d.ts +252 -0
- package/dist/types/adapters/memory/sqliteVector.d.ts.map +1 -0
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
- package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
- package/dist/types/core/agent/memoryRecallInjections.d.ts +13 -3
- package/dist/types/core/agent/memoryRecallInjections.d.ts.map +1 -1
- package/dist/types/core/agent/stages/deliver.d.ts +6 -3
- package/dist/types/core/agent/stages/deliver.d.ts.map +1 -1
- package/dist/types/core/slots/buildSystemPromptSlot.d.ts.map +1 -1
- package/dist/types/core/slots/helpers.d.ts +11 -2
- package/dist/types/core/slots/helpers.d.ts.map +1 -1
- package/dist/types/embedders/index.d.ts.map +1 -1
- package/dist/types/events/payloads.d.ts +63 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/events/registry.d.ts +3 -1
- package/dist/types/events/registry.d.ts.map +1 -1
- package/dist/types/hosting/sqliteSessions.d.ts +7 -13
- package/dist/types/hosting/sqliteSessions.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/fnv1a.d.ts +17 -0
- package/dist/types/lib/fnv1a.d.ts.map +1 -0
- package/dist/types/lib/injection-engine/types.d.ts +17 -0
- package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
- package/dist/types/lib/rag/defineRAG.d.ts +126 -26
- package/dist/types/lib/rag/defineRAG.d.ts.map +1 -1
- package/dist/types/lib/rag/index.d.ts +1 -1
- package/dist/types/lib/rag/index.d.ts.map +1 -1
- package/dist/types/lib/rag/indexDocuments.d.ts +26 -4
- package/dist/types/lib/rag/indexDocuments.d.ts.map +1 -1
- package/dist/types/lib/sqliteUnavailable.d.ts +56 -0
- package/dist/types/lib/sqliteUnavailable.d.ts.map +1 -0
- package/dist/types/memory/define.d.ts.map +1 -1
- package/dist/types/memory/define.types.d.ts +93 -1
- package/dist/types/memory/define.types.d.ts.map +1 -1
- package/dist/types/memory/embedding/embedMessages.d.ts.map +1 -1
- package/dist/types/memory/embedding/emitEmbedding.d.ts +39 -0
- package/dist/types/memory/embedding/emitEmbedding.d.ts.map +1 -0
- package/dist/types/memory/embedding/loadRelevant.d.ts +49 -15
- package/dist/types/memory/embedding/loadRelevant.d.ts.map +1 -1
- package/dist/types/memory/embedding/mockEmbedder.d.ts.map +1 -1
- package/dist/types/memory/embedding/types.d.ts +24 -0
- package/dist/types/memory/embedding/types.d.ts.map +1 -1
- package/dist/types/memory/index.d.ts +2 -1
- package/dist/types/memory/index.d.ts.map +1 -1
- package/dist/types/memory/pipeline/semantic.d.ts +15 -0
- package/dist/types/memory/pipeline/semantic.d.ts.map +1 -1
- package/dist/types/memory/retrieval/index.d.ts +11 -0
- package/dist/types/memory/retrieval/index.d.ts.map +1 -0
- package/dist/types/memory/retrieval/provenance.d.ts +44 -0
- package/dist/types/memory/retrieval/provenance.d.ts.map +1 -0
- package/dist/types/memory/retrieval/topK.d.ts +68 -0
- package/dist/types/memory/retrieval/topK.d.ts.map +1 -0
- package/dist/types/memory/retrieval/types.d.ts +190 -0
- package/dist/types/memory/retrieval/types.d.ts.map +1 -0
- package/dist/types/memory/stages/formatDefault.d.ts +52 -26
- package/dist/types/memory/stages/formatDefault.d.ts.map +1 -1
- package/dist/types/memory/stages/pickByBudget.d.ts.map +1 -1
- package/dist/types/memory/stages/types.d.ts +15 -0
- package/dist/types/memory/stages/types.d.ts.map +1 -1
- package/dist/types/memory/wire/mountMemoryPipeline.d.ts +25 -0
- package/dist/types/memory/wire/mountMemoryPipeline.d.ts.map +1 -1
- package/dist/types/memory-providers.d.ts +2 -0
- package/dist/types/memory-providers.d.ts.map +1 -1
- package/dist/types/observe.d.ts +2 -0
- package/dist/types/observe.d.ts.map +1 -1
- package/dist/types/recorders/core/EmbeddingRecorder.d.ts +20 -0
- package/dist/types/recorders/core/EmbeddingRecorder.d.ts.map +1 -0
- 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
|