renor-engine 0.0.0-stage → 1.0.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/LICENSE +661 -0
- package/NOTICE +73 -0
- package/README.md +105 -2
- package/dist/BUILD.json +6 -0
- package/dist/api/apiAuth.js +43 -0
- package/dist/api/server.js +172 -0
- package/dist/buildIdentity.js +132 -0
- package/dist/cli-extractors.js +46 -0
- package/dist/cli.js +491 -0
- package/dist/config.js +65 -0
- package/dist/context/composer.js +320 -0
- package/dist/context/readHonesty.js +478 -0
- package/dist/context/rowStore.js +238 -0
- package/dist/conversation/formatter.js +81 -0
- package/dist/conversation/parser.js +189 -0
- package/dist/conversation/types.js +2 -0
- package/dist/conversation/writer.js +42 -0
- package/dist/db/client.js +147 -0
- package/dist/db/indexSalvage.js +81 -0
- package/dist/db/migrations.js +642 -0
- package/dist/db/readCache.js +109 -0
- package/dist/db/schema.js +5 -0
- package/dist/db/statements.js +89 -0
- package/dist/digests/digestCache.js +142 -0
- package/dist/digests/digests.js +597 -0
- package/dist/digests/worker.js +348 -0
- package/dist/embeddings/caching.js +337 -0
- package/dist/embeddings/coalescing.js +177 -0
- package/dist/embeddings/interactive.js +103 -0
- package/dist/embeddings/local.js +68 -0
- package/dist/embeddings/openai.js +105 -0
- package/dist/embeddings/rebuild.js +166 -0
- package/dist/embeddings/simdScan.js +258 -0
- package/dist/embeddings/space.js +21 -0
- package/dist/embeddings/types.js +14 -0
- package/dist/embeddings/vectorIndex.js +675 -0
- package/dist/embeddings/vertex.js +224 -0
- package/dist/eval/artifacts.js +42 -0
- package/dist/eval/cacheReport.js +139 -0
- package/dist/eval/comparison.js +182 -0
- package/dist/eval/context700-candidate.js +145 -0
- package/dist/eval/evalCache.js +525 -0
- package/dist/eval/hashEmbedding.js +35 -0
- package/dist/eval/longmemeval.js +56 -0
- package/dist/eval/metrics.js +181 -0
- package/dist/eval/renorbenchBaseline.js +486 -0
- package/dist/eval/renorbenchDocuments.js +134 -0
- package/dist/eval/renorbenchProduct.js +182 -0
- package/dist/eval/runLedger.js +279 -0
- package/dist/eval/runManifest.js +479 -0
- package/dist/eval/scoring.js +197 -0
- package/dist/evidence/attach.js +267 -0
- package/dist/evidence/background.js +147 -0
- package/dist/evidence/evidenceCache.js +182 -0
- package/dist/evidence/gate.js +37 -0
- package/dist/evidence/keyPool.js +85 -0
- package/dist/evidence/ollamaChat.js +84 -0
- package/dist/evidence/optin.js +165 -0
- package/dist/evidence/options.js +130 -0
- package/dist/evidence/secondPass.js +456 -0
- package/dist/extraction/classifyCapture.js +34 -0
- package/dist/extraction/decision-model.js +174 -0
- package/dist/extraction/eventExtractor.js +113 -0
- package/dist/extraction/extractionClient.js +122 -0
- package/dist/extraction/factExtractor.js +46 -0
- package/dist/extraction/factsWritePath.js +179 -0
- package/dist/extraction/preferenceExtractor.js +153 -0
- package/dist/extraction/secretGuard.js +176 -0
- package/dist/extraction/secretScanner.js +665 -0
- package/dist/extraction/typedFacts.js +224 -0
- package/dist/extraction/writePathAdjudication.js +110 -0
- package/dist/importing/apply.js +644 -0
- package/dist/importing/bundle-write.js +80 -0
- package/dist/importing/cli.js +407 -0
- package/dist/importing/existing.js +49 -0
- package/dist/importing/facts.js +123 -0
- package/dist/importing/forgetImported.js +79 -0
- package/dist/importing/hosted.js +467 -0
- package/dist/importing/index.js +29 -0
- package/dist/importing/journalLog.js +96 -0
- package/dist/importing/markdown.js +98 -0
- package/dist/importing/plan.js +206 -0
- package/dist/importing/secretScreen.js +113 -0
- package/dist/importing/serialize.js +188 -0
- package/dist/importing/source.js +463 -0
- package/dist/importing/types.js +6 -0
- package/dist/indexer/chunker.js +319 -0
- package/dist/indexer/dailyEntries.js +63 -0
- package/dist/indexer/hasher.js +10 -0
- package/dist/indexer/laneRuns.js +48 -0
- package/dist/indexer/o200k.js +481 -0
- package/dist/indexer/o200k.json +8 -0
- package/dist/indexer/outsideRoot.js +77 -0
- package/dist/indexer/parser.js +167 -0
- package/dist/indexer/pipeline.js +1168 -0
- package/dist/indexer/staleness.js +201 -0
- package/dist/indexer/tokenizer.js +77 -0
- package/dist/indexer/walker.js +184 -0
- package/dist/mcp/hive.js +88 -0
- package/dist/mcp/requestErrors.js +47 -0
- package/dist/mcp/server.js +396 -0
- package/dist/memory/agentLease.js +43 -0
- package/dist/memory/confinedWrite.js +315 -0
- package/dist/memory/controls.js +73 -0
- package/dist/memory/dailyNote.js +265 -0
- package/dist/memory/durableAppend.js +57 -0
- package/dist/memory/factConflicts.js +203 -0
- package/dist/memory/factTerms.js +321 -0
- package/dist/memory/hiveNameHeader.cjs +10 -0
- package/dist/memory/inventory.js +55 -0
- package/dist/memory/invisibleText.js +20 -0
- package/dist/memory/lifecycle.js +1756 -0
- package/dist/memory/limits.js +13 -0
- package/dist/memory/requests.js +183 -0
- package/dist/memory/sourceName.js +200 -0
- package/dist/memory/whatsNew.js +158 -0
- package/dist/memory/writer.js +175 -0
- package/dist/memory/writerConflicts.js +85 -0
- package/dist/memorymap/memoryMap.js +1568 -0
- package/dist/memorymap/replacedJournal.js +58 -0
- package/dist/memorymap/replacements.js +405 -0
- package/dist/mvp/activity.js +86 -0
- package/dist/mvp/agentMemory.js +1744 -0
- package/dist/mvp/auth.js +531 -0
- package/dist/mvp/cli.js +73 -0
- package/dist/mvp/client-store.js +462 -0
- package/dist/mvp/compileCache.js +249 -0
- package/dist/mvp/compileSeed.js +27 -0
- package/dist/mvp/config.js +288 -0
- package/dist/mvp/connectionStatus.js +80 -0
- package/dist/mvp/contextRelevance.js +413 -0
- package/dist/mvp/engine.js +1065 -0
- package/dist/mvp/exportAll.js +359 -0
- package/dist/mvp/factLanePack.js +77 -0
- package/dist/mvp/factMarkers.js +49 -0
- package/dist/mvp/factsLane.js +203 -0
- package/dist/mvp/hiddenSpans.js +585 -0
- package/dist/mvp/history.js +413 -0
- package/dist/mvp/hitDates.js +31 -0
- package/dist/mvp/hitFloor.js +36 -0
- package/dist/mvp/identity.js +18 -0
- package/dist/mvp/idle.js +113 -0
- package/dist/mvp/memoryDir.js +98 -0
- package/dist/mvp/posthogMcp.js +210 -0
- package/dist/mvp/readScope.js +104 -0
- package/dist/mvp/readUsefulness.js +222 -0
- package/dist/mvp/sanitize.js +48 -0
- package/dist/mvp/schemaRejections.js +91 -0
- package/dist/mvp/scope.js +127 -0
- package/dist/mvp/server.js +2089 -0
- package/dist/mvp/serverCard.js +29 -0
- package/dist/mvp/storedSize.js +139 -0
- package/dist/mvp/telemetry.js +441 -0
- package/dist/retrieval/adviceLines.js +195 -0
- package/dist/retrieval/adviceQuestion.js +44 -0
- package/dist/retrieval/conversationShape.js +44 -0
- package/dist/retrieval/dailyEntryTime.js +55 -0
- package/dist/retrieval/entityLookup.js +113 -0
- package/dist/retrieval/factsFirst.js +188 -0
- package/dist/retrieval/keyword.js +84 -0
- package/dist/retrieval/ledgerEventTime.js +51 -0
- package/dist/retrieval/ledgerLiveness.js +83 -0
- package/dist/retrieval/ledgerPresentation.js +161 -0
- package/dist/retrieval/noteIdentity.js +57 -0
- package/dist/retrieval/orchestrator.js +1086 -0
- package/dist/retrieval/queryHelpers.js +142 -0
- package/dist/retrieval/questionSpan.js +475 -0
- package/dist/retrieval/readerEvents.js +690 -0
- package/dist/retrieval/readerLayout.js +503 -0
- package/dist/retrieval/relativeTimeHints.js +113 -0
- package/dist/retrieval/semantic.js +67 -0
- package/dist/router/routerCache.js +106 -0
- package/dist/router/sessionRouter.js +726 -0
- package/dist/toolRefusal.js +120 -0
- package/dist/watcher/watcher.js +75 -0
- package/package.json +120 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tableHasColumns = tableHasColumns;
|
|
4
|
+
exports.inReadSnapshot = inReadSnapshot;
|
|
5
|
+
exports.databaseStamp = databaseStamp;
|
|
6
|
+
exports.readUntilWrite = readUntilWrite;
|
|
7
|
+
const statements_1 = require("./statements");
|
|
8
|
+
/**
|
|
9
|
+
* Two things a read asks of the database again and again and that change rarely: the shape of a table, and a value
|
|
10
|
+
* derived from every row of one (the ledger's closed events). A `get_context` asked for each of them six times.
|
|
11
|
+
*/
|
|
12
|
+
const withColumns = new WeakMap();
|
|
13
|
+
/**
|
|
14
|
+
* Whether `table` has every one of `columns` (the reads that predate a column ask, so an old index still answers).
|
|
15
|
+
* Only a yes is kept for the connection: a table that lacks a column may get it from a migration, and a column that
|
|
16
|
+
* is there is never dropped, so a no is asked again each time and a yes never is. The names are the code's own, never
|
|
17
|
+
* a caller's.
|
|
18
|
+
*/
|
|
19
|
+
function tableHasColumns(db, table, columns) {
|
|
20
|
+
const key = `${table}(${columns.join(',')})`;
|
|
21
|
+
let known = withColumns.get(db);
|
|
22
|
+
if (known?.has(key))
|
|
23
|
+
return true;
|
|
24
|
+
const present = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name));
|
|
25
|
+
if (!columns.every(c => present.has(c)))
|
|
26
|
+
return false;
|
|
27
|
+
if (!known)
|
|
28
|
+
withColumns.set(db, known = new Map());
|
|
29
|
+
known.set(key, true);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const kept = new WeakMap();
|
|
33
|
+
/** Values kept per connection. A key may carry what a caller sent (a scope), so the oldest goes when the room is used. */
|
|
34
|
+
const MAX_KEPT = 64;
|
|
35
|
+
/** The connections inside a snapshot (`inReadSnapshot`). */
|
|
36
|
+
const snapshots = new WeakSet();
|
|
37
|
+
/**
|
|
38
|
+
* What the database has been through: changes this connection committed (`total_changes()`, which counts every row an
|
|
39
|
+
* INSERT, UPDATE or DELETE touched, triggers and virtual tables included) and changes any other connection committed
|
|
40
|
+
* (`PRAGMA data_version`, which a connection's own writes do not move). Both only ever go up, so two equal stamps are
|
|
41
|
+
* the same data. Null inside a transaction: it can still be rolled back, and its changes would be counted for good. A
|
|
42
|
+
* snapshot is the exception: it is ended with a COMMIT and never rolled back, so what it has counted stays counted.
|
|
43
|
+
*/
|
|
44
|
+
function stampOf(db) {
|
|
45
|
+
if (db.inTransaction && !snapshots.has(db))
|
|
46
|
+
return null;
|
|
47
|
+
const version = (0, statements_1.prepareOnce)(db, 'PRAGMA data_version').pluck().get();
|
|
48
|
+
const changes = (0, statements_1.prepareOnce)(db, 'SELECT total_changes()').pluck().get();
|
|
49
|
+
return `${version}:${changes}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs `read` in ONE transaction. A statement that runs on its own begins a read transaction and ends it again, and in WAL
|
|
53
|
+
* mode that is two locks on the shared-memory index and a file-size call: about 5 microseconds for a lookup that takes 1.5,
|
|
54
|
+
* and a `get_context` runs a hundred of them. Inside a transaction the lock is taken once, and every statement after the first
|
|
55
|
+
* pays for what it reads. The reads see the database as it was when the first of them began; a commit by another process
|
|
56
|
+
* meanwhile is in the next read, not in a mix of the two.
|
|
57
|
+
*
|
|
58
|
+
* `read` is for a stretch of reads that holds no `await` and writes nothing: nothing else of this process can run in a
|
|
59
|
+
* stretch that does not yield, and a write inside it would not be durable until the transaction ended. When a transaction is
|
|
60
|
+
* open already (a write transaction, or a snapshot outside this one) `read` just runs in it. The stamps (`databaseStamp`,
|
|
61
|
+
* `readUntilWrite`) hold for a snapshot, which is committed and never rolled back.
|
|
62
|
+
*/
|
|
63
|
+
function inReadSnapshot(db, read) {
|
|
64
|
+
if (db.inTransaction)
|
|
65
|
+
return read();
|
|
66
|
+
(0, statements_1.prepareOnce)(db, 'BEGIN').run();
|
|
67
|
+
snapshots.add(db);
|
|
68
|
+
let done = false;
|
|
69
|
+
try {
|
|
70
|
+
const result = read();
|
|
71
|
+
done = true;
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
snapshots.delete(db);
|
|
76
|
+
try {
|
|
77
|
+
(0, statements_1.prepareOnce)(db, 'COMMIT').run();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (done)
|
|
81
|
+
throw err; // the read failed first: that is the error to report
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** The stamp `readUntilWrite` keeps a value under. Null inside a transaction, which can still roll back. */
|
|
86
|
+
function databaseStamp(db) {
|
|
87
|
+
return stampOf(db);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `compute()` for the database as it is now, read once and kept until anything is written to it, by this connection or
|
|
91
|
+
* another. The same answer a fresh `compute()` gives: nothing is kept across a write, and nothing at all inside a
|
|
92
|
+
* transaction. `compute` must read only the database, and what it returns is shared: callers must not change it.
|
|
93
|
+
*/
|
|
94
|
+
function readUntilWrite(db, key, compute) {
|
|
95
|
+
const stamp = stampOf(db);
|
|
96
|
+
if (stamp === null)
|
|
97
|
+
return compute();
|
|
98
|
+
let byKey = kept.get(db);
|
|
99
|
+
if (!byKey)
|
|
100
|
+
kept.set(db, byKey = new Map());
|
|
101
|
+
const held = byKey.get(key);
|
|
102
|
+
if (held && held.stamp === stamp)
|
|
103
|
+
return held.value;
|
|
104
|
+
const value = compute();
|
|
105
|
+
if (!byKey.has(key) && byKey.size >= MAX_KEPT)
|
|
106
|
+
byKey.delete(byKey.keys().next().value);
|
|
107
|
+
byKey.set(key, { stamp, value });
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runMigrations = void 0;
|
|
4
|
+
var migrations_1 = require("./migrations");
|
|
5
|
+
Object.defineProperty(exports, "runMigrations", { enumerable: true, get: function () { return migrations_1.runMigrations; } });
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prepareOnce = prepareOnce;
|
|
4
|
+
exports.listStatement = listStatement;
|
|
5
|
+
exports.paddedList = paddedList;
|
|
6
|
+
exports.transactionOnce = transactionOnce;
|
|
7
|
+
/**
|
|
8
|
+
* One prepared statement per connection and SQL text, for the statements a read or a save runs again on every request.
|
|
9
|
+
*
|
|
10
|
+
* Preparing a statement costs a parse and a plan, and a `get_context` runs a few dozen small ones: preparing them again
|
|
11
|
+
* on every call was about 2 ms of its 20 (profiled with scripts/bench/mcp-latency.ts --cpu-profile). A prepared
|
|
12
|
+
* statement is reused as it is, and SQLite prepares it again by itself when the schema changes, so a migration that runs
|
|
13
|
+
* after the first use is seen. The result of a statement does not depend on how long ago it was prepared.
|
|
14
|
+
*
|
|
15
|
+
* What is shared is the statement, so what a caller leaves on it is shared too:
|
|
16
|
+
* - Do not leave a mode on it (`.pluck()`, `.raw()`, `.expand()`, `.safeIntegers()`, `.bind()`) unless every caller of
|
|
17
|
+
* that text asks for the same mode. Give a statement that is read another way its own text.
|
|
18
|
+
* - Do not cache a statement that is iterated (`.iterate()`): an open iteration holds it, and a second use fails.
|
|
19
|
+
* - Do not cache SQL whose text varies with its input (an `IN (?, ?, ...)` list of any length): each text is kept.
|
|
20
|
+
* The cache stops growing at MAX_PREPARED texts per connection; past it a statement is prepared and not kept.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_PREPARED = 256;
|
|
23
|
+
const prepared = new WeakMap();
|
|
24
|
+
function prepareOnce(db, sql) {
|
|
25
|
+
let byText = prepared.get(db);
|
|
26
|
+
if (!byText)
|
|
27
|
+
prepared.set(db, byText = new Map());
|
|
28
|
+
let statement = byText.get(sql);
|
|
29
|
+
if (statement)
|
|
30
|
+
return statement;
|
|
31
|
+
statement = db.prepare(sql);
|
|
32
|
+
if (byText.size < MAX_PREPARED)
|
|
33
|
+
byText.set(sql, statement);
|
|
34
|
+
return statement;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The most values a list is padded to a power of two for. A longer list is prepared as it is and not kept: the padding would
|
|
38
|
+
* double it, and a list that long is not the usual one (SQLite takes 32,766 values in a statement).
|
|
39
|
+
*/
|
|
40
|
+
const MAX_PADDED_LIST = 256;
|
|
41
|
+
/** The number of values a list of `count` is padded to: the next power of two, and at least four. */
|
|
42
|
+
function paddedSize(count) {
|
|
43
|
+
return count <= 4 ? 4 : 2 ** (32 - Math.clz32(count - 1));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A statement over an `IN (?, ?, ...)` list whose length follows the question (the notes a read hit, the entities of those
|
|
47
|
+
* notes). The text of such a statement changes with the length, so `prepareOnce` would keep one for every length and fill its
|
|
48
|
+
* room (see the third rule above), and preparing it on every call was about a tenth of a `get_context`'s statement time (ten
|
|
49
|
+
* statements prepared again on each call). Here the list is made a power of two long, at least four, and the values are padded
|
|
50
|
+
* with `paddedList`: a NULL in an IN list matches no row, so the rows are the rows of the list itself. A connection prepares
|
|
51
|
+
* one statement for each size, a few in all, and the next call finds them ready. `build` makes the SQL from the placeholders.
|
|
52
|
+
* A list over MAX_PADDED_LIST is prepared as it is, for this call only. Use it where the list is the whole of an IN: not
|
|
53
|
+
* under NOT IN, where a NULL in the list would make the whole test unknown.
|
|
54
|
+
*/
|
|
55
|
+
function listStatement(db, count, build) {
|
|
56
|
+
if (count > MAX_PADDED_LIST)
|
|
57
|
+
return db.prepare(build('?,'.repeat(count - 1) + '?'));
|
|
58
|
+
return prepareOnce(db, build('?,'.repeat(paddedSize(count) - 1) + '?'));
|
|
59
|
+
}
|
|
60
|
+
/** The values of a list as `listStatement` binds them: padded with NULL to the size it was prepared for. */
|
|
61
|
+
function paddedList(values) {
|
|
62
|
+
const padded = values.slice();
|
|
63
|
+
if (values.length > MAX_PADDED_LIST)
|
|
64
|
+
return padded;
|
|
65
|
+
for (let size = paddedSize(values.length); padded.length < size;)
|
|
66
|
+
padded.push(null);
|
|
67
|
+
return padded;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One transaction wrapper per connection and body, for a transaction a replay runs once per ledger event.
|
|
71
|
+
*
|
|
72
|
+
* `db.transaction(fn)` builds the wrapper's four variants (default, deferred, immediate, exclusive) every time it is called, which
|
|
73
|
+
* is about 0.05 ms: the ledger replay at startup called it twice per event, about 35 ms of the 190 ms replay of a 1,000-fact
|
|
74
|
+
* workspace. The wrapper does not depend on what the body closes over, so the body takes the connection and its inputs as
|
|
75
|
+
* arguments, is a module-level function, and is wrapped once. What runs in the transaction, and when it commits or rolls back, is
|
|
76
|
+
* unchanged; a call inside another transaction is a savepoint, as before.
|
|
77
|
+
*/
|
|
78
|
+
const transactions = new WeakMap();
|
|
79
|
+
function transactionOnce(db, body) {
|
|
80
|
+
let byBody = transactions.get(db);
|
|
81
|
+
if (!byBody)
|
|
82
|
+
transactions.set(db, byBody = new Map());
|
|
83
|
+
let tx = byBody.get(body);
|
|
84
|
+
if (!tx) {
|
|
85
|
+
tx = db.transaction((...args) => body(db, ...args));
|
|
86
|
+
byBody.set(body, tx);
|
|
87
|
+
}
|
|
88
|
+
return tx;
|
|
89
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DigestCache = exports.DIGEST_CACHE_SCHEMA_VERSION = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
11
|
+
/**
|
|
12
|
+
* Content-addressed store for per-note digests (RENOR_DIGESTS, src/digests/digests.ts).
|
|
13
|
+
*
|
|
14
|
+
* digest key = sha256(schema \0 model \0 reasoning effort \0 sha256(prompt template) \0 input version \0 note_hash)
|
|
15
|
+
*
|
|
16
|
+
* A note's digest is keyed by the hash of the note's file (the index's `note_hash`), so an unchanged note is never
|
|
17
|
+
* digested twice, an edited note gets a new digest, and a changed prompt, model or input rule re-digests every note.
|
|
18
|
+
* An empty digest (the note states nothing about the user's life) is stored too, so it is not asked for again.
|
|
19
|
+
*
|
|
20
|
+
* The item vectors live beside the digest, one set per embedding space (provider, model, dimensions): a digest
|
|
21
|
+
* outlives a change of embedding model, only its vectors are rebuilt. A vector from another space is never read.
|
|
22
|
+
*
|
|
23
|
+
* Design follows src/router/routerCache.ts: SQLite + WAL + busy_timeout=10000 + INSERT OR IGNORE.
|
|
24
|
+
*/
|
|
25
|
+
exports.DIGEST_CACHE_SCHEMA_VERSION = 'note-digest-v1';
|
|
26
|
+
const sha256 = (text) => node_crypto_1.default.createHash('sha256').update(text).digest('hex');
|
|
27
|
+
class DigestCache {
|
|
28
|
+
db;
|
|
29
|
+
getDigestStmt;
|
|
30
|
+
putDigestStmt;
|
|
31
|
+
getVectorsStmt;
|
|
32
|
+
putVectorsStmt;
|
|
33
|
+
constructor(dbPath, opts = {}) {
|
|
34
|
+
if (!opts.readonly)
|
|
35
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dbPath), { recursive: true });
|
|
36
|
+
this.db = new better_sqlite3_1.default(dbPath, opts.readonly ? { readonly: true, fileMustExist: true } : undefined);
|
|
37
|
+
if (!opts.readonly) {
|
|
38
|
+
this.db.pragma('journal_mode = WAL');
|
|
39
|
+
this.db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS note_digests (
|
|
41
|
+
cache_key TEXT PRIMARY KEY,
|
|
42
|
+
schema_version TEXT NOT NULL,
|
|
43
|
+
model TEXT NOT NULL,
|
|
44
|
+
note_hash TEXT NOT NULL,
|
|
45
|
+
items_json TEXT NOT NULL,
|
|
46
|
+
created_at TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS digest_vectors (
|
|
49
|
+
cache_key TEXT NOT NULL,
|
|
50
|
+
space TEXT NOT NULL,
|
|
51
|
+
dims INTEGER NOT NULL,
|
|
52
|
+
vectors BLOB NOT NULL,
|
|
53
|
+
PRIMARY KEY (cache_key, space)
|
|
54
|
+
);
|
|
55
|
+
-- What the background worker (src/digests/worker.ts) spent per UTC day: the per-workspace cost guard reads it.
|
|
56
|
+
CREATE TABLE IF NOT EXISTS digest_spend (
|
|
57
|
+
day TEXT PRIMARY KEY,
|
|
58
|
+
calls INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
micro_usd INTEGER NOT NULL DEFAULT 0
|
|
62
|
+
);
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
this.db.pragma('busy_timeout = 10000');
|
|
66
|
+
this.getDigestStmt = this.db.prepare('SELECT items_json, model FROM note_digests WHERE cache_key = ?');
|
|
67
|
+
this.putDigestStmt = this.db.prepare(`INSERT OR IGNORE INTO note_digests (cache_key, schema_version, model, note_hash, items_json, created_at)
|
|
68
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
69
|
+
this.getVectorsStmt = this.db.prepare('SELECT dims, vectors FROM digest_vectors WHERE cache_key = ? AND space = ?');
|
|
70
|
+
this.putVectorsStmt = this.db.prepare('INSERT OR IGNORE INTO digest_vectors (cache_key, space, dims, vectors) VALUES (?, ?, ?, ?)');
|
|
71
|
+
}
|
|
72
|
+
static digestKey(model, effort, promptTemplate, inputVersion, noteHash) {
|
|
73
|
+
return sha256([exports.DIGEST_CACHE_SCHEMA_VERSION, model, effort, sha256(promptTemplate), inputVersion, noteHash].join('\0'));
|
|
74
|
+
}
|
|
75
|
+
getDigest(key) {
|
|
76
|
+
const row = this.getDigestStmt.get(key);
|
|
77
|
+
if (!row)
|
|
78
|
+
return null;
|
|
79
|
+
return { items: JSON.parse(row.items_json), model: row.model };
|
|
80
|
+
}
|
|
81
|
+
putDigest(key, noteHash, entry) {
|
|
82
|
+
this.putDigestStmt.run(key, exports.DIGEST_CACHE_SCHEMA_VERSION, entry.model, noteHash, JSON.stringify(entry.items), new Date().toISOString());
|
|
83
|
+
}
|
|
84
|
+
/** The item vectors of one digest in one embedding space, or null when they are absent or do not match the items. */
|
|
85
|
+
getVectors(key, space, count) {
|
|
86
|
+
const row = this.getVectorsStmt.get(key, space);
|
|
87
|
+
if (!row || row.dims <= 0 || row.vectors.length !== count * row.dims * 4)
|
|
88
|
+
return null;
|
|
89
|
+
const floats = new Float32Array(row.vectors.buffer.slice(row.vectors.byteOffset, row.vectors.byteOffset + row.vectors.length));
|
|
90
|
+
const out = [];
|
|
91
|
+
for (let i = 0; i < count; i++)
|
|
92
|
+
out.push(Array.from(floats.subarray(i * row.dims, (i + 1) * row.dims)));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
putVectors(key, space, vectors) {
|
|
96
|
+
if (vectors.length === 0)
|
|
97
|
+
return;
|
|
98
|
+
const dims = vectors[0].length;
|
|
99
|
+
const floats = new Float32Array(vectors.length * dims);
|
|
100
|
+
vectors.forEach((vector, i) => floats.set(vector, i * dims));
|
|
101
|
+
this.putVectorsStmt.run(key, space, dims, Buffer.from(floats.buffer));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Keep only the digests of the notes whose file hash is in `noteHashes`, and the vectors that go with them: a digest
|
|
105
|
+
* of text a note no longer holds (it was edited, forgotten, or its file is gone) is derived from content the user
|
|
106
|
+
* removed, so it is not kept. Only the workspace's own cache calls this: an eval cache is shared by many vaults.
|
|
107
|
+
* Returns how many digests it removed.
|
|
108
|
+
*/
|
|
109
|
+
retainOnly(noteHashes) {
|
|
110
|
+
const rows = this.db.prepare('SELECT cache_key, note_hash FROM note_digests').all();
|
|
111
|
+
const stale = rows.filter(row => !noteHashes.has(row.note_hash));
|
|
112
|
+
if (stale.length === 0)
|
|
113
|
+
return 0;
|
|
114
|
+
const dropDigest = this.db.prepare('DELETE FROM note_digests WHERE cache_key = ?');
|
|
115
|
+
const dropVectors = this.db.prepare('DELETE FROM digest_vectors WHERE cache_key = ?');
|
|
116
|
+
this.db.transaction(() => {
|
|
117
|
+
for (const row of stale) {
|
|
118
|
+
dropVectors.run(row.cache_key);
|
|
119
|
+
dropDigest.run(row.cache_key);
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
return stale.length;
|
|
123
|
+
}
|
|
124
|
+
/** What the worker has spent on `day` (a UTC `YYYY-MM-DD`), in USD. */
|
|
125
|
+
spentUsd(day) {
|
|
126
|
+
const row = this.db.prepare('SELECT micro_usd FROM digest_spend WHERE day = ?').get(day);
|
|
127
|
+
return (row?.micro_usd ?? 0) / 1e6;
|
|
128
|
+
}
|
|
129
|
+
addSpend(day, promptTokens, completionTokens, usd) {
|
|
130
|
+
this.db.prepare(`INSERT INTO digest_spend (day, calls, prompt_tokens, completion_tokens, micro_usd) VALUES (?, 1, ?, ?, ?)
|
|
131
|
+
ON CONFLICT(day) DO UPDATE SET calls = calls + 1, prompt_tokens = prompt_tokens + excluded.prompt_tokens,
|
|
132
|
+
completion_tokens = completion_tokens + excluded.completion_tokens, micro_usd = micro_usd + excluded.micro_usd`).run(day, Math.max(0, Math.round(promptTokens)), Math.max(0, Math.round(completionTokens)), Math.max(0, Math.round(usd * 1e6)));
|
|
133
|
+
}
|
|
134
|
+
/** Spend rows older than the newest `keepDays` days are dropped, so the table does not grow without bound. */
|
|
135
|
+
pruneSpend(keepDays) {
|
|
136
|
+
this.db.prepare('DELETE FROM digest_spend WHERE day NOT IN (SELECT day FROM digest_spend ORDER BY day DESC LIMIT ?)').run(keepDays);
|
|
137
|
+
}
|
|
138
|
+
close() {
|
|
139
|
+
this.db.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.DigestCache = DigestCache;
|