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,413 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FACT_CEILING = exports.SEARCH_RELEVANCE_GUIDANCE = exports.CONTEXT_BUDGET_KEEPS_NEWEST = exports.CONTEXT_RELEVANCE_KEEPS_MATCH = exports.CONTEXT_RELEVANCE_GUIDANCE = exports.UNKNOWN_NAME_MARGIN = exports.notMentioned = exports.NO_STRONG_MATCH = exports.CALIBRATED_BARS = exports.CONTEXT_RELEVANCE_DEFAULT_ON = exports.CONTEXT_RELEVANCE_MIN_ENV = exports.CONTEXT_RELEVANCE_FLAG = void 0;
4
+ exports.requestNames = requestNames;
5
+ exports.contextRelevanceEnabled = contextRelevanceEnabled;
6
+ exports.embeddingModelKey = embeddingModelKey;
7
+ exports.resolveRelevanceBar = resolveRelevanceBar;
8
+ exports.contextRelevanceArmed = contextRelevanceArmed;
9
+ exports.renderFactForRelevance = renderFactForRelevance;
10
+ exports.factLane = factLane;
11
+ exports.similaritiesTo = similaritiesTo;
12
+ exports.warmFactVectors = warmFactVectors;
13
+ exports.combineSimilarity = combineSimilarity;
14
+ exports.assessContextRelevance = assessContextRelevance;
15
+ exports.assessQueryRelevance = assessQueryRelevance;
16
+ const readCache_1 = require("../db/readCache");
17
+ const statements_1 = require("../db/statements");
18
+ const factTerms_1 = require("../memory/factTerms");
19
+ const factLanePack_1 = require("./factLanePack");
20
+ const scope_1 = require("./scope");
21
+ /**
22
+ * get_context relevance signal: "does anything on file actually match the request?"
23
+ *
24
+ * Without it an agent gets the same five confident-looking memories for a question
25
+ * nothing on file answers, and fills the gap by guessing. The signal is the best
26
+ * embedding similarity between the query and (a) the note chunks get_context returned
27
+ * and (b) a clean "attribute: value" rendering of every current fact, compared with a
28
+ * bar calibrated per embedding model on fixtures/relevance (method, confusion matrices
29
+ * and the chosen bar: docs/evals/results/RELEVANCE-SIGNAL-20261002.md).
30
+ *
31
+ * Why not the lexical term coverage RENOR_READ_HONESTY uses: coverage counts query
32
+ * words found in the best item, so a paraphrase ("Where am I based?" for
33
+ * home_city = Lisbon) scores 0 even when retrieval found the right memory. Why a fact
34
+ * rendering and not just the ledger chunk: the chunk is mostly event boilerplate (ids,
35
+ * timestamps, "kind: store"), which flattens every similarity toward the same value;
36
+ * the rendering is only the claim.
37
+ *
38
+ * The signal is cheap, and an agent reads `relevant: true` as "the answer is below", so the
39
+ * response holds the match it reports (`matchedOn`), the closest fact or note chunk: the
40
+ * budget makes room for it, cutting other items if it must (agentMemory.ts), and when it
41
+ * does not fit max_tokens even on its own there is no verdict rather than one about something
42
+ * the response lacks. The current fact closest to the request, when it clears the bar on its
43
+ * own (`closestFact`), is listed when the words of the request did not find it: in their
44
+ * place when they found none, else after the facts they found, in the room the rest leaves
45
+ * unless it is the match reported. It never takes the place of a fact they found: a
46
+ * rendering has no subject, so the closest one is often a fact that mentions the project
47
+ * ("role: product manager for Atlas" is near every Atlas question) or another ticket's
48
+ * status, and room taken from the rest for it cost answers. Nothing else is added, removed or
49
+ * reordered, and it adds no embedding call to get_context: the query vector comes from the
50
+ * semantic search that already ran, and fact vectors are embedded once, in the background.
51
+ * Two cases are judged without a cosine to compare: a read that found no note by meaning and
52
+ * holds no current fact is "not relevant" (similarity null), and a request that names
53
+ * something nothing in the scope read mentions needs a match closer by UNKNOWN_NAME_MARGIN.
54
+ * It is absent (never a guess) whenever it cannot judge: flag off, no query vector, no
55
+ * calibrated bar for the embedding model, or fact vectors still warming - a "not relevant"
56
+ * verdict needs every current fact compared.
57
+ */
58
+ /** RENOR_CONTEXT_RELEVANCE: 1/true/yes/on enables, 0/false/no/off disables. */
59
+ exports.CONTEXT_RELEVANCE_FLAG = 'RENOR_CONTEXT_RELEVANCE';
60
+ /** RENOR_CONTEXT_RELEVANCE_MIN: explicit bar, for an embedding model with no calibrated one. */
61
+ exports.CONTEXT_RELEVANCE_MIN_ENV = 'RENOR_CONTEXT_RELEVANCE_MIN';
62
+ /**
63
+ * Shipped default: on, because the held-out calibration met the pre-registered bar (false abstentions on
64
+ * answerable questions < 5% AND most unanswerable flagged, at both workspace sizes). It can only be on, and
65
+ * can only speak, for an embedding model with a calibrated bar (CALIBRATED_BARS) or an explicit
66
+ * RENOR_CONTEXT_RELEVANCE_MIN. tests/mvp/context-relevance-calibration.test.ts asserts this constant and the
67
+ * recorded calibration cannot disagree.
68
+ */
69
+ exports.CONTEXT_RELEVANCE_DEFAULT_ON = true;
70
+ /**
71
+ * Calibrated bars, keyed `<MVP_EMBEDDING_PROVIDER>:<model>:<dimensions>`. Cosine scales
72
+ * differ by model and output size, so a bar never transfers between them.
73
+ */
74
+ exports.CALIBRATED_BARS = {
75
+ 'vertex:gemini-embedding-001:768': 0.63,
76
+ };
77
+ exports.NO_STRONG_MATCH = 'no stored fact or note closely matches this request; the listed items are only the nearest ones';
78
+ /** When `names` were looked for and nothing on file mentions them. */
79
+ const notMentioned = (names) => `nothing on file mentions ${names.join(', ')}; the listed items are only the nearest ones`;
80
+ exports.notMentioned = notMentioned;
81
+ /**
82
+ * How much closer a match must be when the request names something that nothing on file mentions ("What is the
83
+ * Quillmoor pricing?" on a workspace that never said Quillmoor): such a request is about something else than anything
84
+ * on file, and its match is on the generic part ("pricing"). Live, those sat at 0.646-0.651 over the 0.63 bar. Chosen
85
+ * on the dev questions, where a name a semantic model reads across languages ("Wie heißt meine Frau?", 0.708) must
86
+ * stay relevant, and checked on the held-out ones: no answerable question is lost
87
+ * (tests/mvp/context-relevance-calibration.test.ts).
88
+ */
89
+ exports.UNKNOWN_NAME_MARGIN = 0.05;
90
+ /** Capitalized in English, but a time rather than the name of something on file. */
91
+ const CALENDAR_WORDS = new Set([
92
+ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
93
+ 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december',
94
+ ]);
95
+ /**
96
+ * The names in a request: capitalized words that do not open a sentence ("Quillmoor" in "What is the Quillmoor
97
+ * pricing?"), without a possessive. Not "I", a weekday or a month. A sentence's first word is capitalized for that
98
+ * alone, so it is never taken for a name.
99
+ */
100
+ function requestNames(query) {
101
+ const names = new Set();
102
+ let opening = true;
103
+ for (const [token] of query.matchAll(/[\p{L}\p{N}][\p{L}\p{N}\p{M}'’-]*|[.?!:;]+(?=\s|$)|[¿¡]/gu)) {
104
+ if (!/^[\p{L}\p{N}]/u.test(token)) {
105
+ opening = true;
106
+ continue;
107
+ }
108
+ const word = token.replace(/['’]s$/i, '').replace(/['’-]+$/, '');
109
+ if (!opening && /^\p{Lu}/u.test(word) && !/^I(['’]\p{L}+)?$/u.test(word) && !CALENDAR_WORDS.has(word.toLowerCase()))
110
+ names.add(word);
111
+ opening = false;
112
+ }
113
+ return [...names];
114
+ }
115
+ function contextRelevanceEnabled(env = process.env) {
116
+ const raw = (env[exports.CONTEXT_RELEVANCE_FLAG] ?? '').trim().toLowerCase();
117
+ if (raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on')
118
+ return true;
119
+ if (raw === '0' || raw === 'false' || raw === 'no' || raw === 'off')
120
+ return false;
121
+ return exports.CONTEXT_RELEVANCE_DEFAULT_ON;
122
+ }
123
+ function embeddingModelKey(provider, model, dims) {
124
+ return `${provider}:${model ?? ''}:${dims ?? ''}`;
125
+ }
126
+ function resolveRelevanceBar(modelKey, env = process.env) {
127
+ const raw = (env[exports.CONTEXT_RELEVANCE_MIN_ENV] ?? '').trim();
128
+ if (raw !== '') {
129
+ const explicit = Number(raw);
130
+ if (Number.isFinite(explicit))
131
+ return explicit;
132
+ }
133
+ return modelKey && modelKey in exports.CALIBRATED_BARS ? exports.CALIBRATED_BARS[modelKey] : null;
134
+ }
135
+ /** Whether get_context can return the signal, i.e. whether agents should be told about it: on, embeddings configured, a bar to judge by. */
136
+ function contextRelevanceArmed(engine, env = process.env) {
137
+ return Boolean(engine.provider) && contextRelevanceEnabled(env) && resolveRelevanceBar(engine.modelKey, env) !== null;
138
+ }
139
+ /** Appended to the get_context tool description only while the signal is armed. */
140
+ exports.CONTEXT_RELEVANCE_GUIDANCE = ' If the result has relevance.relevant === false, nothing on file closely matches this request: '
141
+ + 'the listed items are only the nearest ones, so do not present them as an answer unless one clearly '
142
+ + 'answers it; say you have no memory of it and ask the user.';
143
+ /**
144
+ * Appended to the get_context description after the guidance, while the signal is armed: the one case in which
145
+ * max_tokens does not drop the item that ranks last first (agentMemory.ts keeps what a relevant verdict was judged on).
146
+ */
147
+ exports.CONTEXT_RELEVANCE_KEEPS_MATCH = ' When relevance.relevant is true, the closest match is in the result, or the newer relevant match the budget kept in its place: max_tokens keeps one of them even if another item has to be dropped to make room for it.';
148
+ /**
149
+ * Appended to the get_context description after the two sentences above, while the signal is armed (the model has a bar to
150
+ * judge by): what the budget keeps first when the matches do not all fit (agentMemory.ts newestMatchAbove, sessionMatesAbove).
151
+ */
152
+ exports.CONTEXT_BUDGET_KEEPS_NEWEST = ' When the matches do not all fit, the newest match above the relevance bar is kept first, with the pieces of its session that say most about the question (at most four, in at most 60% of the budget), then the best-scoring ones, so the latest relevant statement is not the one the budget drops.';
153
+ /**
154
+ * Appended to the search_memory description while the signal is armed. `relevance.fact` is search's alone, and only
155
+ * for the one case where the page could not carry the match: the fact is named, not listed.
156
+ */
157
+ exports.SEARCH_RELEVANCE_GUIDANCE = ' A relevance object says whether anything on file closely matches the query: relevant is true only when the closest match is among the hits, and when it is false the hits are only the nearest ones, so do not present them as an answer unless one clearly answers it.'
158
+ + ' When relevant is true and the closest match is a remembered fact that top_k left no room to list, relevance.fact names it (subject, predicate, object, scope); you can answer from it, and the hits are still at most top_k in all.';
159
+ /**
160
+ * Most current facts the lane compares per call. A tenant with more is not silently truncated: the lane is
161
+ * reported incomplete, so it can still prove relevance but can never be the basis of "nothing matches".
162
+ * The fact list is read once until the next write. Each call then dots the query with the unit vectors
163
+ * already stored for those facts (scripts/eval/relevance-latency.ts measured the uncached list).
164
+ */
165
+ exports.FACT_CEILING = 5_000;
166
+ /** Vectors of no-longer-current facts kept before the store is pruned to the live set. */
167
+ const STALE_VECTORS_KEPT = 256;
168
+ /** Texts per embedding request, and requests in flight, while fact vectors are filled. */
169
+ const FILL_BATCH = 32;
170
+ const FILL_CONCURRENCY = 4;
171
+ /** After a failed fill, how long reads leave the embedding backend alone before trying again. */
172
+ const FILL_RETRY_MS = 30_000;
173
+ /** Per embedding provider, so two models (or two test engines) never share vectors. */
174
+ const slots = new WeakMap();
175
+ function slotFor(provider) {
176
+ let slot = slots.get(provider);
177
+ if (!slot)
178
+ slots.set(provider, slot = { vectors: new Map(), filling: null, retryAfter: 0, epoch: 0, packed: new Map() });
179
+ return slot;
180
+ }
181
+ function renderFactForRelevance(predicate, object) {
182
+ return `${predicate.replace(/_/g, ' ')}: ${object}`;
183
+ }
184
+ function unit(v) {
185
+ let sum = 0;
186
+ for (let i = 0; i < v.length; i++)
187
+ sum += v[i] * v[i];
188
+ const norm = Math.sqrt(sum) || 1;
189
+ const out = new Float32Array(v.length);
190
+ for (let i = 0; i < v.length; i++)
191
+ out[i] = v[i] / norm;
192
+ return out;
193
+ }
194
+ function dot(a, b) {
195
+ let s = 0;
196
+ for (let i = 0; i < a.length; i++)
197
+ s += a[i] * b[i];
198
+ return s;
199
+ }
200
+ /** Embeds `texts` into the slot, FILL_CONCURRENCY requests at a time. A failed batch leaves its texts for the next call. */
201
+ async function fill(provider, slot, texts) {
202
+ const batches = [];
203
+ for (let i = 0; i < texts.length; i += FILL_BATCH)
204
+ batches.push(texts.slice(i, i + FILL_BATCH));
205
+ let next = 0;
206
+ const worker = async () => {
207
+ for (let b = next++; b < batches.length; b = next++) {
208
+ const batch = batches[b];
209
+ const fresh = await provider.embed(batch);
210
+ if (fresh.length !== batch.length)
211
+ throw new Error('embedding provider returned a wrong vector count');
212
+ batch.forEach((text, i) => slot.vectors.set(text, unit(fresh[i])));
213
+ slot.epoch++;
214
+ }
215
+ };
216
+ await Promise.all(Array.from({ length: Math.min(FILL_CONCURRENCY, batches.length) }, worker));
217
+ }
218
+ /** What a lane was asked for (any scope, or one; how many facts at most): the key its snapshot and its pack are kept under. */
219
+ function factLaneKey(scope, ceiling) {
220
+ return `${scope === undefined ? '*' : (0, scope_1.normalizeScope)(scope)}\0${ceiling}`;
221
+ }
222
+ function loadFactSnapshot(db, scope, ceiling) {
223
+ const inScope = scope === undefined ? undefined : (0, scope_1.savedScopeSqlPredicate)((0, scope_1.normalizeScope)(scope), 'f.scope'); // any spelling of it
224
+ const select = `SELECT f.id AS id, f.predicate AS predicate, f.object_text AS object
225
+ FROM facts f
226
+ WHERE f.valid_to IS NULL AND f.source_path = '_mcp_'${inScope ? ` AND ${inScope.sql}` : ''}`;
227
+ const scopeArgs = inScope?.params ?? [];
228
+ let rows = (0, statements_1.prepareOnce)(db, `${select} LIMIT ?`).all(...scopeArgs, ceiling + 1);
229
+ const truncated = rows.length > ceiling;
230
+ if (truncated) {
231
+ // Newest by row id: a reverse scan of the primary key, no sort. Only positives can come of this subset.
232
+ rows = (0, statements_1.prepareOnce)(db, `${select} ORDER BY f.id DESC LIMIT ?`).all(...scopeArgs, ceiling);
233
+ }
234
+ // Each rendering once, with the newest fact rendered that way: the one a match is shown as.
235
+ const newest = new Map();
236
+ for (const r of rows) {
237
+ const rendering = renderFactForRelevance(r.predicate, r.object);
238
+ const seen = newest.get(rendering);
239
+ if (seen === undefined || r.id > seen)
240
+ newest.set(rendering, r.id);
241
+ }
242
+ return { pairs: [...newest], truncated };
243
+ }
244
+ /** The best fact when there is no matrix to read (vectors of two lengths) or the query is of another length: one lookup and one dot per fact. */
245
+ function bestFromSlot(q, snapshot, slot) {
246
+ let best = null;
247
+ let id = -1;
248
+ for (const [rendering, factId] of snapshot.pairs) {
249
+ const v = slot.vectors.get(rendering);
250
+ if (!v)
251
+ continue;
252
+ const s = dot(q, v);
253
+ if (best === null || s > best) {
254
+ best = s;
255
+ id = factId;
256
+ }
257
+ }
258
+ return best === null ? null : { score: best, id };
259
+ }
260
+ /** Drop vectors of facts that are no longer in this snapshot, then embed what is still missing. Same trigger as before. */
261
+ function fillMissing(provider, slot, snapshot, missing) {
262
+ if (missing.length === 0 || slot.filling || Date.now() < slot.retryAfter)
263
+ return;
264
+ if (slot.vectors.size > snapshot.pairs.length + STALE_VECTORS_KEPT) {
265
+ const live = new Set(snapshot.pairs.map(([rendering]) => rendering));
266
+ let dropped = false;
267
+ for (const key of slot.vectors.keys())
268
+ if (!live.has(key)) {
269
+ slot.vectors.delete(key);
270
+ dropped = true;
271
+ }
272
+ // Live rows were not dropped, so a lane packed from them is still the same dots.
273
+ if (dropped)
274
+ slot.epoch++;
275
+ }
276
+ const run = slot.filling = fill(provider, slot, missing)
277
+ .catch(() => { slot.retryAfter = Date.now() + FILL_RETRY_MS; /* advisory: retried later for what is still missing */ })
278
+ .finally(() => { if (slot.filling === run)
279
+ slot.filling = null; });
280
+ }
281
+ /**
282
+ * Best similarity between the query vector and the current facts (scope-filtered).
283
+ * Never waits on the embedding backend: facts without a vector yet start a background
284
+ * fill and are reported through `complete: false`; what is left is a dot product per fact.
285
+ * `complete` is true only when EVERY in-scope current fact was compared: past `ceiling`
286
+ * facts only the newest are, and the lane says so.
287
+ */
288
+ function factLane(db, provider, queryVector, scope, ceiling = exports.FACT_CEILING) {
289
+ const key = factLaneKey(scope, ceiling);
290
+ // The rows change only when something is written. Vectors arrive later, from the background fill.
291
+ const snapshot = (0, readCache_1.readUntilWrite)(db, `factLane\0${key}`, () => loadFactSnapshot(db, scope, ceiling));
292
+ const slot = slotFor(provider);
293
+ let packed = slot.packed.get(key);
294
+ if (!packed || packed.snapshot !== snapshot || packed.epoch !== slot.epoch) {
295
+ packed = (0, factLanePack_1.packLane)(snapshot, slot.vectors, slot.epoch);
296
+ if (slot.packed.size >= 8)
297
+ slot.packed.clear();
298
+ slot.packed.set(key, packed);
299
+ }
300
+ fillMissing(provider, slot, snapshot, packed.missing);
301
+ if (packed.epoch !== slot.epoch)
302
+ packed.epoch = slot.epoch;
303
+ const q = unit(queryVector);
304
+ const found = packed.matrix && q.length === packed.dim
305
+ ? (() => {
306
+ const best = (0, factLanePack_1.bestPacked)(q, packed.matrix, packed.ids.length, packed.dim);
307
+ return best ? { score: best.score, id: packed.ids[best.index] } : null;
308
+ })()
309
+ : bestFromSlot(q, snapshot, slot);
310
+ return {
311
+ similarity: found ? found.score : null,
312
+ complete: !snapshot.truncated && packed.missing.length === 0,
313
+ ...(found ? { bestFactId: found.id } : {}),
314
+ };
315
+ }
316
+ /**
317
+ * Cosine between `queryVector` and each of `texts` (fact renderings, `renderFactForRelevance`), read from the same
318
+ * per-provider store the fact lane fills, through the same embedding provider the notes are searched with. A text the
319
+ * store lacks is embedded now (awaited, FILL_BATCH at a time), so a caller that needs the answer for this read gets it.
320
+ * Null when that fails, or while the backend is in the pause after a failed fill: the caller answers by words instead.
321
+ */
322
+ async function similaritiesTo(provider, texts, queryVector) {
323
+ const slot = slotFor(provider);
324
+ const missing = [...new Set(texts)].filter(text => !slot.vectors.has(text));
325
+ if (missing.length > 0) {
326
+ if (Date.now() < slot.retryAfter)
327
+ return null;
328
+ try {
329
+ await fill(provider, slot, missing);
330
+ }
331
+ catch {
332
+ slot.retryAfter = Date.now() + FILL_RETRY_MS;
333
+ return null;
334
+ }
335
+ }
336
+ const q = unit(queryVector);
337
+ const out = new Map();
338
+ for (const text of texts) {
339
+ const v = slot.vectors.get(text);
340
+ if (v && v.length === q.length)
341
+ out.set(text, dot(q, v));
342
+ }
343
+ return out;
344
+ }
345
+ /** Resolves once the fact vectors a call would need are embedded (or a fill failed): for tests, calibration, warm-up. */
346
+ async function warmFactVectors(db, provider, scope) {
347
+ const probeVector = new Float32Array(provider.dimensions).fill(1);
348
+ slotFor(provider).retryAfter = 0; // an explicit warm-up is not held back by an earlier failure
349
+ for (let attempt = 0; attempt < 4; attempt++) {
350
+ if (factLane(db, provider, probeVector, scope).complete)
351
+ return true;
352
+ await slotFor(provider).filling;
353
+ }
354
+ return factLane(db, provider, probeVector, scope).complete;
355
+ }
356
+ /** The signal is the best match across both lanes; either lane alone may be unavailable. */
357
+ function combineSimilarity(chunk, fact) {
358
+ if (chunk === null)
359
+ return fact;
360
+ if (fact === null)
361
+ return chunk;
362
+ return Math.max(chunk, fact);
363
+ }
364
+ function assessContextRelevance(topSimilarity, bar) {
365
+ if (topSimilarity === null || bar === null || !Number.isFinite(topSimilarity))
366
+ return null;
367
+ // Decide on the rounded value the caller sees, so a reported similarity equal to
368
+ // the bar can never be reported irrelevant.
369
+ const similarity = Math.round(topSimilarity * 1000) / 1000;
370
+ const relevant = similarity >= bar;
371
+ return { relevant, ...(relevant ? {} : { reason: exports.NO_STRONG_MATCH }), similarity, bar };
372
+ }
373
+ /** Advisory by design: any failure here yields no signal rather than a failed get_context. */
374
+ function assessQueryRelevance(input) {
375
+ const { db, engine, scope, probe, env } = input;
376
+ const bar = resolveRelevanceBar(engine.modelKey, env);
377
+ // No query vector means retrieval embedded nothing (empty index): the signal does not pay for one.
378
+ if (bar === null || !engine.provider || !probe.queryVector)
379
+ return null;
380
+ try {
381
+ const chunk = probe.topSimilarity ?? null;
382
+ const fact = factLane(db, engine.provider, probe.queryVector, scope);
383
+ if (input.components) {
384
+ input.components.chunk = chunk;
385
+ input.components.fact = fact.similarity;
386
+ }
387
+ const best = combineSimilarity(chunk, fact.similarity);
388
+ // Nothing to compare: the read found no note by meaning and holds no current fact, so nothing on file is close to
389
+ // the request (withheld while fact vectors are warming, as any "not relevant" is).
390
+ if (best === null)
391
+ return fact.complete ? { relevance: { relevant: false, reason: exports.NO_STRONG_MATCH, similarity: null, bar } } : null;
392
+ // A match just over the bar for a request that names something nothing on file mentions has to be closer.
393
+ const raised = Math.round((bar + exports.UNKNOWN_NAME_MARGIN) * 1000) / 1000;
394
+ const unknown = input.onFile && assessContextRelevance(best, bar)?.relevant && !assessContextRelevance(best, raised)?.relevant
395
+ ? requestNames(input.query ?? '').filter(name => !input.onFile(name))
396
+ : [];
397
+ const judgedBar = unknown.length > 0 ? raised : bar;
398
+ const verdict = assessContextRelevance(best, judgedBar);
399
+ // A match found among some facts proves relevance; "nothing matches" needs all of them compared.
400
+ if (!verdict || (!verdict.relevant && !fact.complete))
401
+ return null;
402
+ if (unknown.length > 0)
403
+ verdict.reason = (0, exports.notMentioned)(unknown);
404
+ const closestFact = fact.bestFactId !== undefined && assessContextRelevance(fact.similarity, judgedBar)?.relevant
405
+ ? (0, factTerms_1.currentFactById)(db, fact.bestFactId)
406
+ : undefined;
407
+ const matchedOn = !verdict.relevant ? undefined : chunk === null || (fact.similarity !== null && fact.similarity >= chunk) ? 'fact' : 'chunk';
408
+ return { relevance: verdict, ...(closestFact ? { closestFact } : {}), ...(matchedOn ? { matchedOn } : {}) };
409
+ }
410
+ catch {
411
+ return null;
412
+ }
413
+ }