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,58 @@
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.REPLACED_FILE_NAME = void 0;
7
+ exports.forgetReplacementsOf = forgetReplacementsOf;
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const path_1 = __importDefault(require("path"));
11
+ /** The replacement journal (a correction that names the memory it corrects), beside the retirement journal. */
12
+ exports.REPLACED_FILE_NAME = 'replaced.jsonl';
13
+ /**
14
+ * Drop every replacement record that names one of `memoryIds` as the memory it corrects or the correction that replaces it,
15
+ * so a permanently deleted memory leaves no id behind (export keeps these records). `retiredDir` is the retirement folder,
16
+ * already inside the vault. The file is replaced atomically; a torn or unreadable line is kept as it is, and a file that is
17
+ * not a plain single-name file is refused. Idempotent, so a retry after a crash repeats it harmlessly.
18
+ */
19
+ async function forgetReplacementsOf(retiredDir, memoryIds) {
20
+ if (memoryIds.size === 0)
21
+ return;
22
+ const file = path_1.default.join(retiredDir, exports.REPLACED_FILE_NAME);
23
+ let st;
24
+ try {
25
+ st = await promises_1.default.lstat(file);
26
+ }
27
+ catch (err) {
28
+ if (err.code === 'ENOENT')
29
+ return;
30
+ throw err;
31
+ }
32
+ if (!st.isFile() || st.nlink !== 1)
33
+ throw new Error(`refusing to rewrite ${JSON.stringify(exports.REPLACED_FILE_NAME)}: it is not a regular file with a single name`);
34
+ const raw = await promises_1.default.readFile(file, 'utf8');
35
+ let changed = false;
36
+ const kept = [];
37
+ for (const line of raw.split('\n')) {
38
+ if (!line.trim())
39
+ continue;
40
+ let hit = false;
41
+ try {
42
+ const r = JSON.parse(line);
43
+ hit = memoryIds.has(String(r.old)) || memoryIds.has(String(r.by));
44
+ }
45
+ catch {
46
+ // A torn final line is kept as it is; the reader skips it.
47
+ }
48
+ if (hit)
49
+ changed = true;
50
+ else
51
+ kept.push(line);
52
+ }
53
+ if (!changed)
54
+ return;
55
+ const tmp = `${file}.${process.pid}.${crypto_1.default.randomBytes(4).toString('hex')}.tmp`;
56
+ await promises_1.default.writeFile(tmp, kept.map((line) => `${line}\n`).join(''), { flag: 'wx', mode: st.mode & 0o777 });
57
+ await promises_1.default.rename(tmp, file);
58
+ }
@@ -0,0 +1,405 @@
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.hasReplacements = hasReplacements;
7
+ exports.replacementIndex = replacementIndex;
8
+ exports.allReplacementRecords = allReplacementRecords;
9
+ exports.assertReplaceable = assertReplaceable;
10
+ exports.recordReplacement = recordReplacement;
11
+ exports.withReplacements = withReplacements;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const promises_1 = __importDefault(require("fs/promises"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const crypto_1 = __importDefault(require("crypto"));
16
+ const dailyNote_1 = require("../memory/dailyNote");
17
+ const scope_1 = require("../mvp/scope");
18
+ const toolRefusal_1 = require("../toolRefusal");
19
+ const sourceName_1 = require("../memory/sourceName");
20
+ const orchestrator_1 = require("../retrieval/orchestrator");
21
+ const memoryMap_1 = require("./memoryMap");
22
+ const replacedJournal_1 = require("./replacedJournal");
23
+ // ---------------------------------------------------------------------------
24
+ // Replacements: a correction that names the earlier memory it corrects.
25
+ //
26
+ // Agents already save corrections ("the export now includes daily entries"), but nothing linked one to the note it
27
+ // corrects, so a recall served the stale note as current (owner request 2026-10-12; two wrong answers the same day).
28
+ // `remember({statement, replaces: <id>})` saves the correction as usual and appends one record here. Nothing stored is
29
+ // touched: the earlier memory keeps its text, its place in search, its export, and stays retrievable. A read that returns it
30
+ // labels it replaced, points at the correction, and lays the correction out before it.
31
+ //
32
+ // The record lives beside the retirement journal (.trash/renor-retired/replaced.jsonl), not in the fact ledger: the
33
+ // ledger is replayed strictly at startup (an unknown event kind refuses to load), so a new kind there would stop an older
34
+ // engine release from starting on a workspace that holds one, and a replacement is no fact.
35
+ //
36
+ // A replacement is in effect while its correction is live. Retiring the correction (retire_memory) ends it; restoring the
37
+ // correction brings it back, with nothing else to undo. Corrections chain (A replaced by B, B by C): A reads as replaced by C.
38
+ // ---------------------------------------------------------------------------
39
+ const REPLACED_FILE = path_1.default.join(memoryMap_1.RETIRED_DIR, replacedJournal_1.REPLACED_FILE_NAME);
40
+ /** The corrected text of a note a replacement pulls into a reply, by characters (an entry is whole). */
41
+ const NOTE_CORRECTION_CHARS = 1200;
42
+ const MAX_CHAIN = 64;
43
+ const EMPTY = { size: 0, replacedBy: () => undefined, replaces: () => [], reaches: () => false, paths: new Set() };
44
+ const relOf = (id) => {
45
+ if (id.startsWith('note:'))
46
+ return id.slice(5);
47
+ const hashAt = id.lastIndexOf('#');
48
+ return id.slice(6, hashAt);
49
+ };
50
+ /**
51
+ * A source as a label or a link carries it: the daily-note header form (letters, digits, hyphens), cut to MAX_SOURCE_LENGTH.
52
+ * Both sides are reduced the same way, so one agent spelled two ways is one source, and a source taken from a note's own front
53
+ * matter is bounded and clean before any reply carries it (memory text can name anything there). Null when nothing is left.
54
+ */
55
+ function sourceForLabel(source) {
56
+ if (typeof source !== 'string')
57
+ return null;
58
+ return (0, sourceName_1.headerSource)(source) || null;
59
+ }
60
+ /**
61
+ * The label for a memory replaced by `replaced`, whose newest correction is `correction` (the last link of its chain). The source
62
+ * is the correction's; the note is added only when both sides' sources are recorded and differ, so a reader can weigh a correction
63
+ * from another agent. A record without `oldSource` (written before it was kept) names no other side.
64
+ */
65
+ function labelOf(replaced, correction) {
66
+ const saver = sourceForLabel(correction.source);
67
+ const label = { by: correction.by, at: correction.at, ...(saver ? { source: saver } : {}) };
68
+ if (replaced.oldSource === undefined)
69
+ return label;
70
+ const from = sourceForLabel(replaced.oldSource);
71
+ if (saver === from)
72
+ return label;
73
+ return { ...label, note: `the correction's source differs from the replaced memory's source (${from ?? 'none recorded'})` };
74
+ }
75
+ let cache;
76
+ /**
77
+ * Whether this workspace has ever recorded a replacement, answered at once (one stat, no await): a read of a workspace with none
78
+ * goes on exactly as it did before replacements existed, with no extra turn of the event loop in its assembly.
79
+ */
80
+ function hasReplacements(memoryDir) {
81
+ try {
82
+ return fs_1.default.lstatSync(path_1.default.join(memoryDir, REPLACED_FILE)).size > 0;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ async function readRecords(ctx) {
89
+ const rootReal = await promises_1.default.realpath(ctx.memoryDir);
90
+ const file = path_1.default.join(rootReal, REPLACED_FILE);
91
+ let stat;
92
+ try {
93
+ stat = await promises_1.default.lstat(file);
94
+ }
95
+ catch (err) {
96
+ if (err.code === 'ENOENT')
97
+ return [];
98
+ throw err;
99
+ }
100
+ const key = `${file}\0${stat.mtimeMs}\0${stat.size}\0${stat.ino}`;
101
+ if (cache?.key === key)
102
+ return cache.records;
103
+ for (const dir of [path_1.default.join(rootReal, '.trash'), path_1.default.join(rootReal, memoryMap_1.RETIRED_DIR)]) {
104
+ const st = await promises_1.default.lstat(dir);
105
+ if (st.isSymbolicLink() || !st.isDirectory())
106
+ throw new Error('the replacement journal is not inside the vault');
107
+ }
108
+ const handle = await (0, memoryMap_1.openConfined)(file);
109
+ let raw;
110
+ try {
111
+ raw = await handle.readFile('utf8');
112
+ }
113
+ finally {
114
+ await handle.close();
115
+ }
116
+ const records = [];
117
+ for (const line of raw.split('\n')) {
118
+ if (!line.trim())
119
+ continue;
120
+ try {
121
+ const r = JSON.parse(line);
122
+ if (typeof r.rid === 'string' && typeof r.old === 'string' && typeof r.by === 'string' && typeof r.at === 'string')
123
+ records.push(r);
124
+ }
125
+ catch {
126
+ // A torn final line (a crash mid-append) is skipped, never fatal.
127
+ }
128
+ }
129
+ cache = { key, records };
130
+ return records;
131
+ }
132
+ /** The replacements in effect: those whose correction is live (not retired), with each chain followed to its newest correction. */
133
+ async function replacementIndex(ctx) {
134
+ const records = await readRecords(ctx);
135
+ if (records.length === 0)
136
+ return EMPTY;
137
+ const retired = new Set((0, memoryMap_1.activeRetired)(await (0, memoryMap_1.readJournal)(ctx)).map(r => r.id));
138
+ const direct = new Map();
139
+ const byAll = new Map();
140
+ for (const r of records) {
141
+ if (retired.has(r.by) || r.old === r.by)
142
+ continue;
143
+ const kept = direct.get(r.old);
144
+ if (!kept || r.at >= kept.at)
145
+ direct.set(r.old, r);
146
+ }
147
+ for (const r of direct.values())
148
+ if (!retired.has(r.old))
149
+ byAll.set(r.by, [...(byAll.get(r.by) ?? []), r.old]);
150
+ if (direct.size === 0)
151
+ return EMPTY;
152
+ // The newest correction of a chain. A loop (or a chain longer than any real one) has no newest: it is read as not replaced,
153
+ // never as replaced by itself or by a middle link.
154
+ const head = (id) => {
155
+ const first = direct.get(id);
156
+ if (!first)
157
+ return undefined;
158
+ let at = first;
159
+ const seen = new Set([id]);
160
+ for (let hops = 0;; hops++) {
161
+ if (seen.has(at.by) || hops > MAX_CHAIN)
162
+ return undefined;
163
+ seen.add(at.by);
164
+ const next = direct.get(at.by);
165
+ if (!next)
166
+ return labelOf(first, at);
167
+ at = next;
168
+ }
169
+ };
170
+ const reaches = (from, target) => {
171
+ const seen = new Set();
172
+ for (let at = from; at !== undefined && !seen.has(at) && seen.size <= MAX_CHAIN; at = direct.get(at)?.by) {
173
+ if (at === target)
174
+ return true;
175
+ seen.add(at);
176
+ }
177
+ return false;
178
+ };
179
+ const paths = new Set();
180
+ for (const r of direct.values()) {
181
+ paths.add(relOf(r.old));
182
+ paths.add(relOf(r.by));
183
+ }
184
+ return { size: direct.size, replacedBy: head, replaces: id => byAll.get(id) ?? [], reaches, paths };
185
+ }
186
+ /** Every record ever written for a correction, in effect or not (export keeps the link of a retired correction). */
187
+ async function allReplacementRecords(ctx) {
188
+ return readRecords(ctx);
189
+ }
190
+ /**
191
+ * Refuse a replacement that cannot hold, before anything is saved: the memory must exist and be live, be a note or a
192
+ * daily-log entry (a fact is corrected by saving it again with the same subject, predicate and scope), and be in the scope the
193
+ * correction is saved to, so a correction never reaches into another scope's memories, nor a read of one scope into another's.
194
+ */
195
+ async function assertReplaceable(ctx, id, scope) {
196
+ if (typeof id === 'string' && id.startsWith('fact:')) {
197
+ throw (0, toolRefusal_1.refusal)('invalid_argument', 'a fact is corrected by saving it again with the same subject, predicate and scope, not with replaces');
198
+ }
199
+ (0, memoryMap_1.parseMemoryId)(id);
200
+ const memory = (await (0, memoryMap_1.readMemory)(ctx, id)).memory;
201
+ if (!(0, scope_1.notePathMatchesScope)(memory.path, scope)) {
202
+ throw (0, toolRefusal_1.refusal)('invalid_argument', 'replaces names a memory of another scope: a correction replaces only memories of its own scope');
203
+ }
204
+ }
205
+ /**
206
+ * The source the memory `id` was saved with, in the label form (null: none recorded), or undefined when it cannot be read: the
207
+ * link is kept without it. A note's source is its own front matter, so it is reduced here, before it is stored or compared.
208
+ */
209
+ async function sourceOfMemory(ctx, id) {
210
+ try {
211
+ return sourceForLabel((await (0, memoryMap_1.readMemory)(ctx, id)).memory.source);
212
+ }
213
+ catch {
214
+ return undefined;
215
+ }
216
+ }
217
+ /**
218
+ * Record that `by` replaces `old`. Null, and nothing recorded, when that would make a loop: the save is the memory it replaces
219
+ * (an identical line saved again in the same second is the same memory, since an id is the hash of the line), or is already
220
+ * replaced by it, directly or down a chain.
221
+ */
222
+ async function recordReplacement(ctx, old, by, source) {
223
+ const existing = (await readRecords(ctx)).find(r => r.old === old && r.by === by);
224
+ if (existing)
225
+ return existing;
226
+ if (old === by || (await replacementIndex(ctx)).reaches(by, old))
227
+ return null;
228
+ const oldSource = await sourceOfMemory(ctx, old);
229
+ const saver = sourceForLabel(source);
230
+ const record = {
231
+ rid: `replaced:${crypto_1.default.randomBytes(8).toString('hex')}`, old, by, at: new Date().toISOString(), ...(saver ? { source: saver } : {}),
232
+ ...(oldSource !== undefined ? { oldSource } : {}),
233
+ };
234
+ const dir = await (0, memoryMap_1.ensureRealDir)(await promises_1.default.realpath(ctx.memoryDir), memoryMap_1.RETIRED_DIR);
235
+ const handle = await promises_1.default.open(path_1.default.join(dir, path_1.default.basename(REPLACED_FILE)), promises_1.default.constants.O_RDWR | promises_1.default.constants.O_APPEND | promises_1.default.constants.O_CREAT | memoryMap_1.NOFOLLOW, 0o600);
236
+ try {
237
+ // A crash can leave a torn last line; start on a fresh line so this record is never glued to it.
238
+ const size = (await handle.stat()).size;
239
+ let prefix = '';
240
+ if (size > 0) {
241
+ const last = Buffer.alloc(1);
242
+ await handle.read(last, 0, 1, size - 1);
243
+ if (last[0] !== 0x0a)
244
+ prefix = '\n';
245
+ }
246
+ await handle.appendFile(`${prefix}${JSON.stringify(record)}\n`, 'utf8');
247
+ await handle.sync();
248
+ }
249
+ finally {
250
+ await handle.close();
251
+ }
252
+ cache = undefined;
253
+ return record;
254
+ }
255
+ /** The memory ids a hit is: the note, or the daily-log entries whose text holds it. A fact or a ledger statement is none. */
256
+ async function hitIds(ctx, index, hit, entries) {
257
+ if (hit.notePath === '' || !index.paths.has(hit.notePath))
258
+ return [];
259
+ if (!dailyNote_1.DAILY_FILE_RE.test(hit.notePath))
260
+ return [`note:${hit.notePath}`];
261
+ let blocks = entries.get(hit.notePath);
262
+ if (!blocks) {
263
+ blocks = (async () => {
264
+ try {
265
+ const file = await (0, memoryMap_1.resolveVaultFile)(ctx, hit.notePath, true);
266
+ const read = await (0, memoryMap_1.readPrefix)(file.real, 8 * 1024 * 1024);
267
+ return read ? (0, memoryMap_1.parseEntries)(file.rel, read.text).blocks : [];
268
+ }
269
+ catch {
270
+ return []; // gone since it was indexed: the hit stays what it was
271
+ }
272
+ })();
273
+ entries.set(hit.notePath, blocks);
274
+ }
275
+ const text = hit.text.trim();
276
+ return (await blocks).filter(b => b.raw.trim() === text || b.text.trim() === text || b.raw.includes(text)).map(b => b.id);
277
+ }
278
+ /** A correction that was not among the hits, as a hit: an entry whole, a note by its first characters. */
279
+ async function correctionHit(ctx, id) {
280
+ try {
281
+ const { memory, text } = await (0, memoryMap_1.readMemory)(ctx, id);
282
+ const body = memory.kind === 'entry' ? text : text.slice(0, NOTE_CORRECTION_CHARS);
283
+ const raw = memory.kind === 'entry' ? await entryRaw(ctx, id) : null;
284
+ const bound = (0, orchestrator_1.createNoteTimeBound)(ctx.db);
285
+ const indexed = path_1.default.resolve(ctx.memoryDir, memory.path);
286
+ const date = memory.session_date ?? (memory.kind === 'entry' ? bound(indexed, memory.at) : (0, orchestrator_1.resolveSessionDate)(undefined, memory.at));
287
+ return {
288
+ notePath: memory.path,
289
+ headingPath: dailyNote_1.DAILY_FILE_RE.exec(memory.path)?.[2] ?? '',
290
+ text: raw ?? body,
291
+ ...(date ? { session_date: date } : {}),
292
+ };
293
+ }
294
+ catch {
295
+ return null;
296
+ }
297
+ }
298
+ async function entryRaw(ctx, id) {
299
+ const { rel } = (0, memoryMap_1.parseMemoryId)(id);
300
+ const file = await (0, memoryMap_1.resolveVaultFile)(ctx, rel, true);
301
+ const read = await (0, memoryMap_1.readPrefix)(file.real, 8 * 1024 * 1024);
302
+ return read ? (0, memoryMap_1.parseEntries)(file.rel, read.text).blocks.find(b => b.id === id)?.raw ?? null : null;
303
+ }
304
+ /**
305
+ * Lay a reply's hits out for the replacements in effect: a hit that is a replaced memory gets `replaced` (its label) and
306
+ * follows its correction, and a correction the read did not return is added before the first memory it replaces, with that
307
+ * memory's score and reason "replacement", so an agent that recalls a stale note is handed the fix with it. Nothing is dropped
308
+ * for it and no text changes. `pairs` receives each replaced hit with the correction listed for it, for a caller that budgets
309
+ * (get_context). `limit` is the most hits the reply may hold: an added correction takes the place of the last hit that is
310
+ * neither a replaced memory nor a correction, so a page never grows past its top_k (and a limit too small for the pair lists the
311
+ * correction and leaves the replaced memory out). A reply none of whose hits is involved is
312
+ * returned as it was, and a workspace with no replacement pays one stat.
313
+ */
314
+ async function withReplacements(ctx, hits, options = {}) {
315
+ if (!hasReplacements(ctx.memoryDir))
316
+ return hits;
317
+ const index = await replacementIndex(ctx);
318
+ if (index.size === 0 || !hits.some(h => index.paths.has(h.notePath)))
319
+ return hits;
320
+ const entries = new Map();
321
+ const ids = await Promise.all(hits.map(h => hitIds(ctx, index, h, entries)));
322
+ const labels = ids.map(own => {
323
+ const labelled = own.map(id => index.replacedBy(id));
324
+ return own.length > 0 && labelled.every(l => l !== undefined) ? labelled[0] : undefined;
325
+ });
326
+ const correctionAt = new Map(); // correction id -> position of the hit that is it
327
+ ids.forEach((own, i) => { if (labels[i] === undefined)
328
+ for (const id of own)
329
+ correctionAt.set(id, i); });
330
+ if (labels.every(l => l === undefined))
331
+ return hits;
332
+ const out = [];
333
+ const waiting = new Map(); // replaced hits held until their correction is out
334
+ const flush = (hitIds) => {
335
+ for (const id of hitIds) {
336
+ for (const held of waiting.get(id) ?? [])
337
+ out.push(held);
338
+ waiting.delete(id);
339
+ }
340
+ };
341
+ const added = new Map(); // correction id -> the hit added for it (null: unreadable)
342
+ const involved = new Set();
343
+ const paired = [];
344
+ for (let i = 0; i < hits.length; i++) {
345
+ const label = labels[i];
346
+ if (label === undefined) {
347
+ out.push(hits[i]);
348
+ flush(ids[i]);
349
+ continue;
350
+ }
351
+ const replaced = { ...hits[i], replaced: label };
352
+ involved.add(replaced);
353
+ paired.push({ replaced, by: label.by });
354
+ if (correctionAt.has(label.by)) {
355
+ // The correction is among the hits: before this one if it came first, else this one waits for it.
356
+ if (correctionAt.get(label.by) < i)
357
+ out.push(replaced);
358
+ else
359
+ waiting.set(label.by, [...(waiting.get(label.by) ?? []), replaced]);
360
+ continue;
361
+ }
362
+ if (!added.has(label.by)) {
363
+ const fix = await correctionHit(ctx, label.by);
364
+ const hit = fix ? { ...fix, ...(hits[i].score !== undefined ? { score: hits[i].score } : {}), reason: 'replacement' } : null;
365
+ added.set(label.by, hit);
366
+ if (hit) {
367
+ out.push(hit);
368
+ involved.add(hit);
369
+ }
370
+ }
371
+ out.push(replaced);
372
+ }
373
+ for (const held of waiting.values())
374
+ out.push(...held);
375
+ for (const { by } of paired)
376
+ if (correctionAt.has(by))
377
+ involved.add(hits[correctionAt.get(by)]);
378
+ if (options.pairs) {
379
+ for (const { replaced, by } of paired) {
380
+ const correction = added.get(by) ?? (correctionAt.has(by) ? hits[correctionAt.get(by)] : null);
381
+ if (correction)
382
+ options.pairs.push({ replaced, correction });
383
+ }
384
+ }
385
+ const limit = options.limit;
386
+ if (limit !== undefined) {
387
+ while (out.length > limit) {
388
+ let at = -1;
389
+ for (let i = out.length - 1; i >= 0; i--)
390
+ if (!involved.has(out[i])) {
391
+ at = i;
392
+ break;
393
+ }
394
+ // Nothing else left to give up: a top_k too small for the pair lists the correction and leaves the replaced memory out.
395
+ if (at < 0)
396
+ for (let i = out.length - 1; i >= 0 && at < 0; i--)
397
+ if (out[i].replaced !== undefined)
398
+ at = i;
399
+ if (at < 0)
400
+ break;
401
+ out.splice(at, 1);
402
+ }
403
+ }
404
+ return out;
405
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActivityGauge = exports.ACTIVITY_PATH = exports.ACTIVITY_REPORT_VERSION = void 0;
4
+ /**
5
+ * What the release asks an engine before it restarts it: is work running that a restart would cut, and when did the last
6
+ * of it end.
7
+ *
8
+ * Always on, unlike the on-demand IdleMonitor. It counts every unit of work a restart could cut and nothing else:
9
+ * - an MCP request (POST or DELETE /mcp) from its start until its HTTP response has finished or closed, which is later
10
+ * than its tool handler returning (a streamed response can still be in delivery), and ends when the client aborts;
11
+ * - every tool call until it returns, even one whose client has gone;
12
+ * - the flights the monitor tracks (evidence extraction, digests);
13
+ * - the process-owned jobs a call starts and does not await (the embedding rebuild, the background index) while they
14
+ * are working. A job that is only waiting out a delay before its next attempt is not work in progress: it holds
15
+ * nothing, and it is resumed from the index at the next start (an embedding outage must not hold a release forever).
16
+ * An open MCP stream (GET /mcp) is not work, so a connected agent that is not calling anything leaves it at zero. It holds
17
+ * a count and a time, never a tool name, an argument or a tenant.
18
+ *
19
+ * With an IdleMonitor (RENOR_ON_DEMAND) tracked work is handed on to it unchanged, so the idle clock sees exactly what it
20
+ * saw before this existed.
21
+ */
22
+ /** Marks the body of GET /internal/activity as this report, so a reader never mistakes another answer for it. */
23
+ exports.ACTIVITY_REPORT_VERSION = 'activity/v1';
24
+ exports.ACTIVITY_PATH = '/internal/activity';
25
+ class ActivityGauge {
26
+ inner;
27
+ now;
28
+ background;
29
+ inFlight = 0;
30
+ lastEnded = null;
31
+ watched = new WeakSet();
32
+ constructor(inner = null, now = Date.now,
33
+ /** The background jobs running now (the embedding rebuild, the background index); asked at every report. */
34
+ background = () => []) {
35
+ this.inner = inner;
36
+ this.now = now;
37
+ this.background = background;
38
+ }
39
+ /** Counts one unit of work from now until the returned function is called (once; further calls do nothing). */
40
+ begin() {
41
+ this.inFlight++;
42
+ let ended = false;
43
+ return () => {
44
+ if (ended)
45
+ return;
46
+ ended = true;
47
+ this.inFlight--;
48
+ this.lastEnded = this.now();
49
+ };
50
+ }
51
+ /** Counts a request until its response has finished or closed (an abort included). */
52
+ request(res) {
53
+ const end = this.begin();
54
+ res.once('close', end);
55
+ }
56
+ /** Same contract as IdleMonitor.track, so it stands in for the monitor wherever work is tracked. */
57
+ async track(work) {
58
+ const end = this.begin();
59
+ try {
60
+ return await (this.inner ? this.inner.track(work) : work());
61
+ }
62
+ finally {
63
+ end();
64
+ }
65
+ }
66
+ report() {
67
+ let jobs = 0;
68
+ for (const job of this.background()) {
69
+ if (!job)
70
+ continue;
71
+ // The moment a job ends is when the last work ended, whoever asks and whenever.
72
+ if (!this.watched.has(job.done)) {
73
+ this.watched.add(job.done);
74
+ void job.done.then(() => { this.lastEnded = this.now(); }, () => { this.lastEnded = this.now(); });
75
+ }
76
+ if (!job.waiting())
77
+ jobs++;
78
+ }
79
+ return {
80
+ renor: exports.ACTIVITY_REPORT_VERSION,
81
+ inFlight: this.inFlight + jobs,
82
+ lastEndedAgoMs: this.lastEnded === null ? null : Math.max(0, Math.floor(this.now() - this.lastEnded)),
83
+ };
84
+ }
85
+ }
86
+ exports.ActivityGauge = ActivityGauge;