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
+ 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.HISTORY_CACHE_CHARS = exports.HistoryUnavailableError = exports.HISTORY_DISCLAIMER = void 0;
7
+ exports.tokenize = tokenize;
8
+ exports.historyCacheChars = historyCacheChars;
9
+ exports.searchHistory = searchHistory;
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ /**
14
+ * Local history search for the single-owner ChatGPT MVP.
15
+ *
16
+ * Searches the owner's PROMOTED ChatGPT history (the explicit additive
17
+ * directory filled by the reviewed promotion command) — never arbitrary vault
18
+ * trees, never the fact memory directory, never raw exports or staging.
19
+ *
20
+ * Deterministic lexical search only: no embeddings, no semantic/vector
21
+ * pipeline, no remote calls. There is NO derived/persistent index to corrupt:
22
+ * every query re-derives eligibility from the authoritative promotion
23
+ * ownership manifest and re-verifies each candidate note's sha256, so
24
+ * restarts reproduce results exactly, and newly added / removed / withdrawn
25
+ * (carried) / tampered / human-edited notes are picked up or excluded on the
26
+ * very next query. The source Markdown and manifest are read-only.
27
+ *
28
+ * Eligibility (manifest is the authority, mirroring promote.py):
29
+ * - only entries of the manifest's `notes` map whose `rel` is `notes/<name>`
30
+ * with a safe `chatgpt-*.md` filename (the MOC and any other metadata are
31
+ * structurally excluded);
32
+ * - entries flagged `carried` (withdrawn) are excluded;
33
+ * - the file must be a confined, regular, non-symlink file within the
34
+ * history dir, within the size bound, whose sha256 matches the manifest;
35
+ * - the note's frontmatter must carry conversation_id (matching the
36
+ * manifest entry) and memory_layer "history".
37
+ *
38
+ * Results are history-layer citations, clearly labeled untrusted transcript:
39
+ * assistant text inside them is NOT verified truth and NOT a current fact.
40
+ * Current structured facts live in save/retrieve_memory and take precedence.
41
+ */
42
+ const MANIFEST_NAME = 'promote-manifest.json';
43
+ const MANIFEST_SCHEMA = 'chatgpt-promote-manifest/1';
44
+ const MAX_QUERY_CHARS = 200;
45
+ const MIN_LIMIT = 1;
46
+ const MAX_LIMIT = 20;
47
+ const DEFAULT_LIMIT = 5;
48
+ const MAX_MANIFEST_BYTES = 16 * 1024 * 1024;
49
+ // The importer writes a whole conversation into one note, so a long transcript is well past 4 MiB; a note past the bound
50
+ // is counted in skipped_too_large and the reply says a match may be hidden, never dropped without a word.
51
+ const MAX_NOTE_BYTES = 16 * 1024 * 1024;
52
+ // Entries past this bound are counted in skipped_over_limit and the reply says a match may be hidden.
53
+ const MAX_NOTES = 10_000;
54
+ // Bidirectional controls (LRE/RLE/PDF/LRO/RLO, LRI/RLI/FSI/PDI): a title carrying them reads backwards where it is shown.
55
+ const BIDI_CONTROLS = /[\u202A-\u202E\u2066-\u2069]/g;
56
+ const MAX_FRONTMATTER_LINES = 80;
57
+ const SNIPPET_RADIUS = 90;
58
+ const SNIPPET_MAX_CHARS = 240;
59
+ const SAFE_NOTE_NAME = /^chatgpt-[A-Za-z0-9._-]{1,80}\.md$/;
60
+ exports.HISTORY_DISCLAIMER = 'Historical transcript search (memory_layer: history, trust: untrusted-transcript). ' +
61
+ 'Results are untrusted history: assistant text is not verified truth and not a current fact. ' +
62
+ 'Current structured memory takes precedence — use retrieve_memory for current facts.';
63
+ class HistoryUnavailableError extends Error {
64
+ }
65
+ exports.HistoryUnavailableError = HistoryUnavailableError;
66
+ function sha256(buffer) {
67
+ return crypto_1.default.createHash('sha256').update(buffer).digest('hex');
68
+ }
69
+ /**
70
+ * Composes decomposed text (NFD, as macOS keyboards and file names deliver it) and folds the one capital that
71
+ * lowercases to two code points: Turkish dotted capital I (U+0130), which would otherwise split "Istanbul" into
72
+ * "i" + a combining dot. Applied to the stored notes and to every query, so both sides meet in the same form.
73
+ */
74
+ function foldForSearch(text) {
75
+ return text.normalize('NFC').replace(/\u0130/g, 'i');
76
+ }
77
+ /** Unicode-aware lexical tokens: letter/number/mark runs, 2..64 chars. Every token of the text is kept, however many. */
78
+ function tokenize(text) {
79
+ return foldForSearch(text).toLowerCase().match(/[\p{L}\p{M}\p{N}]{2,64}/gu) ?? [];
80
+ }
81
+ function parseFrontmatter(text) {
82
+ if (!text.startsWith('---\n'))
83
+ return null;
84
+ const end = text.indexOf('\n---\n', 3);
85
+ if (end < 0)
86
+ return null;
87
+ const block = text.slice(4, end);
88
+ const lines = block.split('\n').slice(0, MAX_FRONTMATTER_LINES);
89
+ const fields = {};
90
+ for (const line of lines) {
91
+ if (!line.trim())
92
+ continue;
93
+ const idx = line.indexOf(': ');
94
+ if (idx <= 0)
95
+ continue;
96
+ const key = line.slice(0, idx).trim();
97
+ const raw = line.slice(idx + 2);
98
+ if (raw.startsWith('"') && raw.endsWith('"')) {
99
+ try {
100
+ fields[key] = JSON.parse(raw);
101
+ }
102
+ catch {
103
+ return null; // malformed quoting: ineligible, fail closed
104
+ }
105
+ }
106
+ else if (raw === 'true' || raw === 'false') {
107
+ fields[key] = raw === 'true';
108
+ }
109
+ else if (/^-?\d+$/.test(raw)) {
110
+ fields[key] = Number(raw);
111
+ }
112
+ else {
113
+ fields[key] = raw;
114
+ }
115
+ }
116
+ return { fields, body: text.slice(end + 5) };
117
+ }
118
+ /** Per history directory, the notes of the last pass: a note that left the manifest drops out with its entry. */
119
+ const noteCache = new Map();
120
+ /**
121
+ * The most body characters the cache keeps, across the directory's notes (about 2x in memory once tokenized). A note
122
+ * past the bound is still searched, read and tokenized on each query as before, so memory stays bounded however large the
123
+ * history grows: the cache is a speed-up for the notes it holds, not a copy of the corpus.
124
+ */
125
+ exports.HISTORY_CACHE_CHARS = 16_000_000;
126
+ /** Body characters the cache holds right now, across every history directory (for tests and diagnostics). */
127
+ function historyCacheChars() {
128
+ let total = 0;
129
+ for (const entries of noteCache.values())
130
+ for (const { note } of entries.values())
131
+ total += note.body.length;
132
+ return total;
133
+ }
134
+ /**
135
+ * Re-derive the eligible set from the authoritative manifest + notes.
136
+ * Everything ineligible is counted, never thrown (bounded, fail-closed).
137
+ */
138
+ function collectEligible(historyDir) {
139
+ const notes = [];
140
+ const cached = noteCache.get(historyDir) ?? new Map();
141
+ const nextCache = new Map();
142
+ let cachedChars = 0;
143
+ // Kept only within HISTORY_CACHE_CHARS. A hit is kept on the same terms, so the bound holds whichever pass a note is in.
144
+ const keep = (name, entry) => {
145
+ const size = entry.note.body.length;
146
+ if (cachedChars + size > exports.HISTORY_CACHE_CHARS)
147
+ return;
148
+ cachedChars += size;
149
+ nextCache.set(name, entry);
150
+ };
151
+ let skipped = 0;
152
+ let tooLarge = 0;
153
+ let overLimit = 0;
154
+ const manifestPath = path_1.default.join(historyDir, MANIFEST_NAME);
155
+ let manifestStat;
156
+ try {
157
+ manifestStat = fs_1.default.lstatSync(manifestPath);
158
+ }
159
+ catch {
160
+ throw new HistoryUnavailableError(`${MANIFEST_NAME} not found in the configured history directory`);
161
+ }
162
+ if (!manifestStat.isFile()) {
163
+ throw new HistoryUnavailableError(`${MANIFEST_NAME} is not a regular file`);
164
+ }
165
+ if (manifestStat.size > MAX_MANIFEST_BYTES) {
166
+ throw new HistoryUnavailableError(`${MANIFEST_NAME} exceeds the size bound`);
167
+ }
168
+ let manifest;
169
+ try {
170
+ manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
171
+ }
172
+ catch {
173
+ throw new HistoryUnavailableError(`${MANIFEST_NAME} is not valid JSON`);
174
+ }
175
+ if (manifest?.schema !== MANIFEST_SCHEMA) {
176
+ throw new HistoryUnavailableError(`${MANIFEST_NAME} has an unexpected schema`);
177
+ }
178
+ const entries = manifest?.notes;
179
+ if (typeof entries !== 'object' || entries === null || Array.isArray(entries)) {
180
+ throw new HistoryUnavailableError(`${MANIFEST_NAME} notes map is malformed`);
181
+ }
182
+ for (const [name, entry] of Object.entries(entries)) {
183
+ if (notes.length + skipped >= MAX_NOTES) {
184
+ skipped++;
185
+ overLimit++;
186
+ continue;
187
+ }
188
+ // structural eligibility: only promoted note filenames, never the MOC or
189
+ // any other metadata; never a path.
190
+ if (!SAFE_NOTE_NAME.test(name)) {
191
+ skipped++;
192
+ continue;
193
+ }
194
+ if (typeof entry !== 'object' || entry === null) {
195
+ skipped++;
196
+ continue;
197
+ }
198
+ if (entry.carried === true) {
199
+ skipped++;
200
+ continue;
201
+ } // withdrawn: excluded
202
+ const rel = entry.rel;
203
+ if (typeof rel !== 'string' || rel !== `notes/${name}`) {
204
+ skipped++;
205
+ continue;
206
+ }
207
+ const expectedSha = entry.note_sha256;
208
+ const convId = entry.conversation_id;
209
+ if (typeof expectedSha !== 'string' || !/^[0-9a-f]{64}$/.test(expectedSha)) {
210
+ skipped++;
211
+ continue;
212
+ }
213
+ if (typeof convId !== 'string' || convId.length === 0 || convId.length > 200) {
214
+ skipped++;
215
+ continue;
216
+ }
217
+ const notePath = path_1.default.join(historyDir, name);
218
+ let stat;
219
+ try {
220
+ stat = fs_1.default.lstatSync(notePath);
221
+ }
222
+ catch {
223
+ skipped++;
224
+ continue;
225
+ }
226
+ // confined, regular, non-symlink file only (no subdirectories, no links)
227
+ if (stat.isSymbolicLink() || !stat.isFile()) {
228
+ skipped++;
229
+ continue;
230
+ }
231
+ if (stat.size > MAX_NOTE_BYTES) {
232
+ skipped++;
233
+ tooLarge++;
234
+ continue;
235
+ }
236
+ const key = `${stat.mtimeMs}:${stat.size}:${expectedSha}:${convId}`;
237
+ const hit = cached.get(name);
238
+ // The cache is trusted for a file whose mtime and size are unchanged: its hash is not checked again on a hit.
239
+ if (hit !== undefined && hit.key === key) {
240
+ keep(name, hit);
241
+ notes.push(hit.note);
242
+ continue;
243
+ }
244
+ let data;
245
+ try {
246
+ data = fs_1.default.readFileSync(notePath);
247
+ }
248
+ catch {
249
+ skipped++;
250
+ continue;
251
+ }
252
+ if (sha256(data) !== expectedSha) {
253
+ skipped++;
254
+ continue;
255
+ } // tampered/human-edited: fail closed
256
+ const parsed = parseFrontmatter(data.toString('utf-8'));
257
+ if (!parsed) {
258
+ skipped++;
259
+ continue;
260
+ }
261
+ const { fields, body } = parsed;
262
+ if (fields.memory_layer !== 'history') {
263
+ skipped++;
264
+ continue;
265
+ }
266
+ if (typeof fields.conversation_id !== 'string' || fields.conversation_id !== convId) {
267
+ skipped++;
268
+ continue;
269
+ }
270
+ const title = typeof fields.title === 'string' ? fields.title.replace(BIDI_CONTROLS, '') : '(untitled conversation)';
271
+ // Index ONLY the visible-transcript section: never frontmatter, never the
272
+ // warning callout, never the importer boilerplate/omission footer.
273
+ const SECTION_MARKER = '## Visible active branch';
274
+ const FOOTER_MARKER = '*(Omitted from this note:';
275
+ let searchable = body;
276
+ const sectionAt = body.indexOf(SECTION_MARKER);
277
+ if (sectionAt >= 0)
278
+ searchable = body.slice(sectionAt + SECTION_MARKER.length);
279
+ // The importer writes its footer as the last line of the note, so the real one is the LAST line-initial marker.
280
+ // A transcript message that quotes the marker mid-line, or earlier in the note, is searched, not cut at.
281
+ searchable = foldForSearch(searchable);
282
+ const footerAt = searchable.lastIndexOf(`\n${FOOTER_MARKER}`);
283
+ if (footerAt >= 0)
284
+ searchable = searchable.slice(0, footerAt);
285
+ const counts = new Map();
286
+ for (const token of tokenize(searchable))
287
+ counts.set(token, (counts.get(token) ?? 0) + 1);
288
+ const note = {
289
+ name,
290
+ conversationId: convId,
291
+ title: title.slice(0, 300),
292
+ created: typeof fields.created === 'string' ? fields.created : null,
293
+ updated: typeof fields.updated === 'string' ? fields.updated : null,
294
+ body: searchable,
295
+ counts,
296
+ };
297
+ keep(name, { key, note });
298
+ notes.push(note);
299
+ }
300
+ noteCache.set(historyDir, nextCache);
301
+ return { notes, skipped, tooLarge, overLimit };
302
+ }
303
+ function buildSnippet(body, terms) {
304
+ const lower = body.toLowerCase();
305
+ let best = -1;
306
+ let bestLen = 0;
307
+ for (const term of terms) {
308
+ const at = lower.indexOf(term);
309
+ if (at >= 0 && (best < 0 || at < best)) {
310
+ best = at;
311
+ bestLen = term.length;
312
+ }
313
+ }
314
+ if (best < 0)
315
+ return '';
316
+ const start = Math.max(0, best - SNIPPET_RADIUS);
317
+ const end = Math.min(body.length, best + bestLen + SNIPPET_RADIUS);
318
+ let snippet = body.slice(start, end);
319
+ snippet = snippet.replace(/\s+/g, ' ').trim();
320
+ if (start > 0)
321
+ snippet = '…' + snippet;
322
+ if (end < body.length)
323
+ snippet = snippet + '…';
324
+ return snippet.slice(0, SNIPPET_MAX_CHARS);
325
+ }
326
+ /** Said in the reply when notes were not searched, so a caller never reads a short list as the whole history. */
327
+ function unsearchedNote(tooLarge, overLimit) {
328
+ if (tooLarge === 0 && overLimit === 0)
329
+ return undefined;
330
+ return `${tooLarge} note(s) over the ${MAX_NOTE_BYTES / (1024 * 1024)} MiB search bound and ${overLimit} `
331
+ + `entr(ies) past the ${MAX_NOTES}-note bound were not searched; a match may be hidden in them.`;
332
+ }
333
+ function searchHistory(historyDir, rawQuery, rawLimit) {
334
+ if (historyDir === null) {
335
+ return {
336
+ ok: true,
337
+ configured: false,
338
+ results: [],
339
+ note: 'history search is not configured (MVP_HISTORY_DIR is not set). '
340
+ + 'Fact memory (save_memory/retrieve_memory/export_memory) is unaffected.',
341
+ disclaimer: exports.HISTORY_DISCLAIMER,
342
+ };
343
+ }
344
+ const query = rawQuery.trim();
345
+ // A query the schema admits (1..200 characters) can still hold no searchable character: trim() strips whitespace
346
+ // (spaces, tabs, newlines, U+2028/U+2029, U+FEFF), so a whitespace-only query trims to empty. It is answered as a
347
+ // search that found nothing — the same reply a punctuation-only query already gets when `terms` comes back empty —
348
+ // instead of throwing, so a schema-valid argument never reaches a raw error and search_history matches search_text,
349
+ // which answers a whitespace query with ok and no results. Only over-length is a real violation here, reachable
350
+ // from a direct caller (the tool caps the query at MAX_QUERY_CHARS).
351
+ if (query.length > MAX_QUERY_CHARS) {
352
+ throw new Error(`query must be 1..${MAX_QUERY_CHARS} characters`);
353
+ }
354
+ const limit = rawLimit === undefined ? DEFAULT_LIMIT : rawLimit;
355
+ if (!Number.isInteger(limit) || limit < MIN_LIMIT || limit > MAX_LIMIT) {
356
+ throw new Error(`limit must be an integer between ${MIN_LIMIT} and ${MAX_LIMIT}`);
357
+ }
358
+ const { notes, skipped, tooLarge, overLimit } = collectEligible(historyDir);
359
+ const unsearched = unsearchedNote(tooLarge, overLimit);
360
+ const terms = Array.from(new Set(tokenize(query)));
361
+ if (terms.length === 0) {
362
+ return {
363
+ ok: true, configured: true, query, total_matches: 0, returned: 0,
364
+ skipped_ineligible: skipped, skipped_too_large: tooLarge, skipped_over_limit: overLimit,
365
+ ...(unsearched ? { note: unsearched } : {}), results: [], disclaimer: exports.HISTORY_DISCLAIMER,
366
+ };
367
+ }
368
+ const termSet = new Set(terms);
369
+ const scored = [];
370
+ for (const note of notes) {
371
+ const counts = new Map();
372
+ for (const [token, count] of note.counts) {
373
+ if (termSet.has(token))
374
+ counts.set(token, count);
375
+ }
376
+ if (counts.size === 0)
377
+ continue;
378
+ let score = 0;
379
+ for (const count of counts.values())
380
+ score += count;
381
+ score += counts.size; // coverage bonus: more distinct matched terms first
382
+ scored.push({ note, score, counts });
383
+ }
384
+ // Deterministic order: score desc, then conversation id asc, then name.
385
+ scored.sort((a, b) => b.score - a.score ||
386
+ a.note.conversationId.localeCompare(b.note.conversationId) ||
387
+ a.note.name.localeCompare(b.note.name));
388
+ const hits = scored.slice(0, limit).map(({ note, score, counts }) => ({
389
+ conversation_id: note.conversationId,
390
+ title: note.title,
391
+ created: note.created,
392
+ updated: note.updated,
393
+ source_path: note.name,
394
+ snippet: buildSnippet(note.body, Array.from(counts.keys())),
395
+ matched_terms: Array.from(counts.keys()).sort(),
396
+ score,
397
+ memory_layer: 'history',
398
+ trust: 'untrusted-transcript',
399
+ }));
400
+ return {
401
+ ok: true,
402
+ configured: true,
403
+ query,
404
+ total_matches: scored.length,
405
+ returned: hits.length,
406
+ skipped_ineligible: skipped,
407
+ skipped_too_large: tooLarge,
408
+ skipped_over_limit: overLimit,
409
+ ...(unsearched ? { note: unsearched } : {}),
410
+ results: hits,
411
+ disclaimer: exports.HISTORY_DISCLAIMER,
412
+ };
413
+ }
@@ -0,0 +1,31 @@
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.withHitDates = withHitDates;
7
+ const path_1 = __importDefault(require("path"));
8
+ const orchestrator_1 = require("../retrieval/orchestrator");
9
+ const dailyEntryTime_1 = require("../retrieval/dailyEntryTime");
10
+ /**
11
+ * The date of a keyword hit, the one search_memory and get_context give the same excerpt: the moment a daily-log entry
12
+ * was saved, otherwise its note's date (front-matter session_date or date, capped at the file's write time, else that
13
+ * time). search_text found a note or an entry by its words and said nothing of when it was written, so an agent could not
14
+ * tell a note from last week from one a year old. A hit that already has a date (a remembered fact, a ledger event) keeps it,
15
+ * and a hit whose note is not in the index stays undated: unknown is never `now`.
16
+ */
17
+ async function withHitDates(db, memoryDir, hits) {
18
+ if (!hits.some(h => h.session_date === undefined && h.notePath !== ''))
19
+ return hits;
20
+ const noteDate = (0, orchestrator_1.createNoteDater)(db);
21
+ const boundTime = (0, orchestrator_1.createNoteTimeBound)(db);
22
+ const entryTime = (0, dailyEntryTime_1.createDailyEntryClock)();
23
+ return Promise.all(hits.map(async (hit) => {
24
+ if (hit.session_date !== undefined || hit.notePath === '')
25
+ return hit;
26
+ const stored = path_1.default.resolve(memoryDir, hit.notePath);
27
+ const entryAt = await entryTime({ notePath: stored, text: hit.text });
28
+ const session_date = entryAt === undefined ? noteDate(stored) : boundTime(stored, entryAt);
29
+ return session_date === undefined ? hit : { ...hit, session_date };
30
+ }));
31
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SEARCH_HIT_FLOOR_ENV = exports.SEARCH_HIT_FLOOR_MODEL_KEY = exports.SEARCH_HIT_FLOOR_DEFAULT = void 0;
4
+ exports.searchHitFloor = searchHitFloor;
5
+ exports.applyHitFloor = applyHitFloor;
6
+ const orchestrator_1 = require("../retrieval/orchestrator");
7
+ /**
8
+ * MCP search_memory removes note hits below a raw-cosine floor after the facts lane fills the page.
9
+ * The default is 0.63 only for Vertex gemini-embedding-001 at 768 dimensions; every other model defaults off.
10
+ * Operator setting RENOR_SEARCH_HIT_FLOOR overrides either default; 0 restores the old response.
11
+ * Facts, evidence, digests, ledger events and graph hits have no own cosine and stay. Session expansion
12
+ * stays only while a matched hit from its note survives. Nothing is added, reordered or backfilled.
13
+ * get_context, HTTP API and CLI searches do not use this floor.
14
+ */
15
+ exports.SEARCH_HIT_FLOOR_DEFAULT = 0.63;
16
+ exports.SEARCH_HIT_FLOOR_MODEL_KEY = 'vertex:gemini-embedding-001:768';
17
+ exports.SEARCH_HIT_FLOOR_ENV = 'RENOR_SEARCH_HIT_FLOOR';
18
+ /** A cosine threshold never transfers between models or dimensions. Invalid settings use this model's default. */
19
+ function searchHitFloor(modelKey, env = process.env) {
20
+ const defaultFloor = modelKey === exports.SEARCH_HIT_FLOOR_MODEL_KEY ? exports.SEARCH_HIT_FLOOR_DEFAULT : 0;
21
+ const value = env[exports.SEARCH_HIT_FLOOR_ENV];
22
+ if (value === undefined || value.trim() === '')
23
+ return defaultFloor;
24
+ const floor = Number(value);
25
+ return Number.isFinite(floor) && floor >= 0 && floor <= 1 ? floor : defaultFloor;
26
+ }
27
+ /**
28
+ * `hits` without the ones found by meaning whose similarity is below `floor`, and without the statements the session
29
+ * expansion added to a note none of whose matched hits is left. `withheld` counts every hit taken out. Order is kept.
30
+ */
31
+ function applyHitFloor(hits, floor) {
32
+ const below = new Set(hits.filter(hit => typeof hit.similarity === 'number' && hit.similarity < floor));
33
+ const matched = new Set(hits.filter(hit => hit.reason !== orchestrator_1.SESSION_EXPAND_REASON && !below.has(hit)).map(hit => hit.notePath));
34
+ const kept = hits.filter(hit => !below.has(hit) && (hit.reason !== orchestrator_1.SESSION_EXPAND_REASON || matched.has(hit.notePath)));
35
+ return { hits: kept, withheld: hits.length - kept.length };
36
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.identityLine = identityLine;
4
+ /**
5
+ * The one line every get_context and memory_status reply carries, so an agent connected to the wrong server, or to a
6
+ * Renor it did not expect, can tell (agent survey P1 and improvement 2, "Absence is silent"). The workspace name is the
7
+ * one the gateway passes in `x-renor-workspace` (it deletes any client-sent copy first); an engine reached without the
8
+ * gateway, or a control plane that does not return the name yet, gets none, and the line says so instead of guessing.
9
+ */
10
+ const NO_NAME = 'hosted Renor, no workspace name passed';
11
+ function identityLine(headers) {
12
+ const raw = headers?.['x-renor-workspace'];
13
+ const value = Array.isArray(raw) ? raw[0] : raw;
14
+ if (typeof value !== 'string')
15
+ return NO_NAME;
16
+ const name = value.replace(/[\u0000-\u001f\u007f"]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80).trim();
17
+ return name === '' ? NO_NAME : `hosted Renor, workspace "${name}"`;
18
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IdleMonitor = exports.DRAINING_HEADER = void 0;
4
+ const activity_1 = require("./activity");
5
+ /**
6
+ * On-demand idle exit (RENOR_ON_DEMAND=1, see config.ts). Off by default: without the
7
+ * switch no monitor is created and the engine runs until it is stopped, as before.
8
+ *
9
+ * Work holds the engine awake: every HTTP request while it is open (an SSE stream
10
+ * included) and every MCP tool call until it returns, even one whose client has gone,
11
+ * because a write or index job outlives its connection. The idle clock restarts when a
12
+ * request ends below 400, so a scanner's 401/404 or a readiness probe on /version does
13
+ * not keep a sleeping workspace resident (nor does the release's look at /internal/activity).
14
+ *
15
+ * With nothing in flight for the whole idle period the monitor starts draining: every
16
+ * later request is answered 503 with the draining header before it does anything, so
17
+ * the gateway knows the engine is stopping (not broken) and can start it again without
18
+ * ever replaying a request that might have been committed.
19
+ */
20
+ /** Response header on a draining engine's 503. The gateway reads it. */
21
+ exports.DRAINING_HEADER = 'x-renor-draining';
22
+ class IdleMonitor {
23
+ idleMs;
24
+ now;
25
+ inFlight = 0;
26
+ lastActivity;
27
+ timer = null;
28
+ started = false;
29
+ draining = false;
30
+ resolveIdle;
31
+ idle = new Promise(resolve => { this.resolveIdle = resolve; });
32
+ constructor(idleMs, now = Date.now) {
33
+ this.idleMs = idleMs;
34
+ this.now = now;
35
+ this.lastActivity = now();
36
+ }
37
+ /** Start the idle clock once the engine listens; resolves when draining begins. */
38
+ start() {
39
+ this.started = true;
40
+ this.lastActivity = this.now();
41
+ this.arm();
42
+ return this.idle;
43
+ }
44
+ get isDraining() {
45
+ return this.draining;
46
+ }
47
+ /** Express middleware: counts each request while it is open; refuses while draining. */
48
+ middleware = (req, res, next) => {
49
+ if (this.draining) {
50
+ res.setHeader('Connection', 'close');
51
+ res.setHeader('Retry-After', '1');
52
+ res.setHeader(exports.DRAINING_HEADER, '1');
53
+ res.status(503).json({ error: 'draining' });
54
+ return;
55
+ }
56
+ const probe = req.method === 'GET' && (req.path === '/version' || req.path === activity_1.ACTIVITY_PATH);
57
+ const end = this.begin();
58
+ let ended = false;
59
+ const finish = () => {
60
+ if (ended)
61
+ return;
62
+ ended = true;
63
+ end(!probe && res.statusCode < 400);
64
+ };
65
+ res.once('finish', finish);
66
+ res.once('close', finish);
67
+ next();
68
+ };
69
+ /** Holds the engine awake for one unit of work, such as an MCP tool call. */
70
+ async track(work) {
71
+ const end = this.begin();
72
+ try {
73
+ return await work();
74
+ }
75
+ finally {
76
+ end(true);
77
+ }
78
+ }
79
+ begin() {
80
+ this.inFlight++;
81
+ if (this.timer) {
82
+ clearTimeout(this.timer);
83
+ this.timer = null;
84
+ }
85
+ return (activity) => {
86
+ this.inFlight--;
87
+ if (activity)
88
+ this.lastActivity = this.now();
89
+ this.arm();
90
+ };
91
+ }
92
+ arm() {
93
+ if (!this.started || this.draining || this.inFlight > 0)
94
+ return;
95
+ if (this.timer)
96
+ clearTimeout(this.timer);
97
+ const wait = Math.max(0, this.lastActivity + this.idleMs - this.now());
98
+ this.timer = setTimeout(() => this.check(), wait);
99
+ this.timer.unref();
100
+ }
101
+ check() {
102
+ this.timer = null;
103
+ if (this.draining || this.inFlight > 0)
104
+ return;
105
+ if (this.now() - this.lastActivity < this.idleMs) {
106
+ this.arm();
107
+ return;
108
+ }
109
+ this.draining = true;
110
+ this.resolveIdle();
111
+ }
112
+ }
113
+ exports.IdleMonitor = IdleMonitor;