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.
Files changed (176) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +73 -0
  3. package/README.md +105 -2
  4. package/dist/BUILD.json +6 -0
  5. package/dist/api/apiAuth.js +43 -0
  6. package/dist/api/server.js +172 -0
  7. package/dist/buildIdentity.js +132 -0
  8. package/dist/cli-extractors.js +46 -0
  9. package/dist/cli.js +491 -0
  10. package/dist/config.js +65 -0
  11. package/dist/context/composer.js +320 -0
  12. package/dist/context/readHonesty.js +478 -0
  13. package/dist/context/rowStore.js +238 -0
  14. package/dist/conversation/formatter.js +81 -0
  15. package/dist/conversation/parser.js +189 -0
  16. package/dist/conversation/types.js +2 -0
  17. package/dist/conversation/writer.js +42 -0
  18. package/dist/db/client.js +147 -0
  19. package/dist/db/indexSalvage.js +81 -0
  20. package/dist/db/migrations.js +642 -0
  21. package/dist/db/readCache.js +109 -0
  22. package/dist/db/schema.js +5 -0
  23. package/dist/db/statements.js +89 -0
  24. package/dist/digests/digestCache.js +142 -0
  25. package/dist/digests/digests.js +597 -0
  26. package/dist/digests/worker.js +348 -0
  27. package/dist/embeddings/caching.js +337 -0
  28. package/dist/embeddings/coalescing.js +177 -0
  29. package/dist/embeddings/interactive.js +103 -0
  30. package/dist/embeddings/local.js +68 -0
  31. package/dist/embeddings/openai.js +105 -0
  32. package/dist/embeddings/rebuild.js +166 -0
  33. package/dist/embeddings/simdScan.js +258 -0
  34. package/dist/embeddings/space.js +21 -0
  35. package/dist/embeddings/types.js +14 -0
  36. package/dist/embeddings/vectorIndex.js +675 -0
  37. package/dist/embeddings/vertex.js +224 -0
  38. package/dist/eval/artifacts.js +42 -0
  39. package/dist/eval/cacheReport.js +139 -0
  40. package/dist/eval/comparison.js +182 -0
  41. package/dist/eval/context700-candidate.js +145 -0
  42. package/dist/eval/evalCache.js +525 -0
  43. package/dist/eval/hashEmbedding.js +35 -0
  44. package/dist/eval/longmemeval.js +56 -0
  45. package/dist/eval/metrics.js +181 -0
  46. package/dist/eval/renorbenchBaseline.js +486 -0
  47. package/dist/eval/renorbenchDocuments.js +134 -0
  48. package/dist/eval/renorbenchProduct.js +182 -0
  49. package/dist/eval/runLedger.js +279 -0
  50. package/dist/eval/runManifest.js +479 -0
  51. package/dist/eval/scoring.js +197 -0
  52. package/dist/evidence/attach.js +267 -0
  53. package/dist/evidence/background.js +147 -0
  54. package/dist/evidence/evidenceCache.js +182 -0
  55. package/dist/evidence/gate.js +37 -0
  56. package/dist/evidence/keyPool.js +85 -0
  57. package/dist/evidence/ollamaChat.js +84 -0
  58. package/dist/evidence/optin.js +165 -0
  59. package/dist/evidence/options.js +130 -0
  60. package/dist/evidence/secondPass.js +456 -0
  61. package/dist/extraction/classifyCapture.js +34 -0
  62. package/dist/extraction/decision-model.js +174 -0
  63. package/dist/extraction/eventExtractor.js +113 -0
  64. package/dist/extraction/extractionClient.js +122 -0
  65. package/dist/extraction/factExtractor.js +46 -0
  66. package/dist/extraction/factsWritePath.js +179 -0
  67. package/dist/extraction/preferenceExtractor.js +153 -0
  68. package/dist/extraction/secretGuard.js +176 -0
  69. package/dist/extraction/secretScanner.js +665 -0
  70. package/dist/extraction/typedFacts.js +224 -0
  71. package/dist/extraction/writePathAdjudication.js +110 -0
  72. package/dist/importing/apply.js +644 -0
  73. package/dist/importing/bundle-write.js +80 -0
  74. package/dist/importing/cli.js +407 -0
  75. package/dist/importing/existing.js +49 -0
  76. package/dist/importing/facts.js +123 -0
  77. package/dist/importing/forgetImported.js +79 -0
  78. package/dist/importing/hosted.js +467 -0
  79. package/dist/importing/index.js +29 -0
  80. package/dist/importing/journalLog.js +96 -0
  81. package/dist/importing/markdown.js +98 -0
  82. package/dist/importing/plan.js +206 -0
  83. package/dist/importing/secretScreen.js +113 -0
  84. package/dist/importing/serialize.js +188 -0
  85. package/dist/importing/source.js +463 -0
  86. package/dist/importing/types.js +6 -0
  87. package/dist/indexer/chunker.js +319 -0
  88. package/dist/indexer/dailyEntries.js +63 -0
  89. package/dist/indexer/hasher.js +10 -0
  90. package/dist/indexer/laneRuns.js +48 -0
  91. package/dist/indexer/o200k.js +481 -0
  92. package/dist/indexer/o200k.json +8 -0
  93. package/dist/indexer/outsideRoot.js +77 -0
  94. package/dist/indexer/parser.js +167 -0
  95. package/dist/indexer/pipeline.js +1168 -0
  96. package/dist/indexer/staleness.js +201 -0
  97. package/dist/indexer/tokenizer.js +77 -0
  98. package/dist/indexer/walker.js +184 -0
  99. package/dist/mcp/hive.js +88 -0
  100. package/dist/mcp/requestErrors.js +47 -0
  101. package/dist/mcp/server.js +396 -0
  102. package/dist/memory/agentLease.js +43 -0
  103. package/dist/memory/confinedWrite.js +315 -0
  104. package/dist/memory/controls.js +73 -0
  105. package/dist/memory/dailyNote.js +265 -0
  106. package/dist/memory/durableAppend.js +57 -0
  107. package/dist/memory/factConflicts.js +203 -0
  108. package/dist/memory/factTerms.js +321 -0
  109. package/dist/memory/hiveNameHeader.cjs +10 -0
  110. package/dist/memory/inventory.js +55 -0
  111. package/dist/memory/invisibleText.js +20 -0
  112. package/dist/memory/lifecycle.js +1756 -0
  113. package/dist/memory/limits.js +13 -0
  114. package/dist/memory/requests.js +183 -0
  115. package/dist/memory/sourceName.js +200 -0
  116. package/dist/memory/whatsNew.js +158 -0
  117. package/dist/memory/writer.js +175 -0
  118. package/dist/memory/writerConflicts.js +85 -0
  119. package/dist/memorymap/memoryMap.js +1568 -0
  120. package/dist/memorymap/replacedJournal.js +58 -0
  121. package/dist/memorymap/replacements.js +405 -0
  122. package/dist/mvp/activity.js +86 -0
  123. package/dist/mvp/agentMemory.js +1744 -0
  124. package/dist/mvp/auth.js +531 -0
  125. package/dist/mvp/cli.js +73 -0
  126. package/dist/mvp/client-store.js +462 -0
  127. package/dist/mvp/compileCache.js +249 -0
  128. package/dist/mvp/compileSeed.js +27 -0
  129. package/dist/mvp/config.js +288 -0
  130. package/dist/mvp/connectionStatus.js +80 -0
  131. package/dist/mvp/contextRelevance.js +413 -0
  132. package/dist/mvp/engine.js +1065 -0
  133. package/dist/mvp/exportAll.js +359 -0
  134. package/dist/mvp/factLanePack.js +77 -0
  135. package/dist/mvp/factMarkers.js +49 -0
  136. package/dist/mvp/factsLane.js +203 -0
  137. package/dist/mvp/hiddenSpans.js +585 -0
  138. package/dist/mvp/history.js +413 -0
  139. package/dist/mvp/hitDates.js +31 -0
  140. package/dist/mvp/hitFloor.js +36 -0
  141. package/dist/mvp/identity.js +18 -0
  142. package/dist/mvp/idle.js +113 -0
  143. package/dist/mvp/memoryDir.js +98 -0
  144. package/dist/mvp/posthogMcp.js +210 -0
  145. package/dist/mvp/readScope.js +104 -0
  146. package/dist/mvp/readUsefulness.js +222 -0
  147. package/dist/mvp/sanitize.js +48 -0
  148. package/dist/mvp/schemaRejections.js +91 -0
  149. package/dist/mvp/scope.js +127 -0
  150. package/dist/mvp/server.js +2089 -0
  151. package/dist/mvp/serverCard.js +29 -0
  152. package/dist/mvp/storedSize.js +139 -0
  153. package/dist/mvp/telemetry.js +441 -0
  154. package/dist/retrieval/adviceLines.js +195 -0
  155. package/dist/retrieval/adviceQuestion.js +44 -0
  156. package/dist/retrieval/conversationShape.js +44 -0
  157. package/dist/retrieval/dailyEntryTime.js +55 -0
  158. package/dist/retrieval/entityLookup.js +113 -0
  159. package/dist/retrieval/factsFirst.js +188 -0
  160. package/dist/retrieval/keyword.js +84 -0
  161. package/dist/retrieval/ledgerEventTime.js +51 -0
  162. package/dist/retrieval/ledgerLiveness.js +83 -0
  163. package/dist/retrieval/ledgerPresentation.js +161 -0
  164. package/dist/retrieval/noteIdentity.js +57 -0
  165. package/dist/retrieval/orchestrator.js +1086 -0
  166. package/dist/retrieval/queryHelpers.js +142 -0
  167. package/dist/retrieval/questionSpan.js +475 -0
  168. package/dist/retrieval/readerEvents.js +690 -0
  169. package/dist/retrieval/readerLayout.js +503 -0
  170. package/dist/retrieval/relativeTimeHints.js +113 -0
  171. package/dist/retrieval/semantic.js +67 -0
  172. package/dist/router/routerCache.js +106 -0
  173. package/dist/router/sessionRouter.js +726 -0
  174. package/dist/toolRefusal.js +120 -0
  175. package/dist/watcher/watcher.js +75 -0
  176. 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;