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
package/dist/cli.js ADDED
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const config_1 = require("./config");
6
+ const client_1 = require("./db/client");
7
+ const schema_1 = require("./db/schema");
8
+ const lifecycle_1 = require("./memory/lifecycle");
9
+ const pipeline_1 = require("./indexer/pipeline");
10
+ const watcher_1 = require("./watcher/watcher");
11
+ const staleness_1 = require("./indexer/staleness");
12
+ const orchestrator_1 = require("./retrieval/orchestrator");
13
+ const readerLayout_1 = require("./retrieval/readerLayout");
14
+ const readerEvents_1 = require("./retrieval/readerEvents");
15
+ const adviceLines_1 = require("./retrieval/adviceLines");
16
+ const ledgerPresentation_1 = require("./retrieval/ledgerPresentation");
17
+ const attach_1 = require("./evidence/attach");
18
+ const options_1 = require("./evidence/options");
19
+ const attach_2 = require("./evidence/attach");
20
+ const vectorIndex_1 = require("./embeddings/vectorIndex");
21
+ const openai_1 = require("./embeddings/openai");
22
+ const local_1 = require("./embeddings/local");
23
+ const server_1 = require("./api/server");
24
+ const apiAuth_1 = require("./api/apiAuth");
25
+ const cli_extractors_1 = require("./cli-extractors");
26
+ const typedFacts_1 = require("./extraction/typedFacts");
27
+ const eventExtractor_1 = require("./extraction/eventExtractor");
28
+ const preferenceExtractor_1 = require("./extraction/preferenceExtractor");
29
+ const composer_1 = require("./context/composer");
30
+ const readHonesty_1 = require("./context/readHonesty");
31
+ const server_2 = require("./mcp/server");
32
+ const engine_1 = require("./mvp/engine");
33
+ const hashEmbedding_1 = require("./eval/hashEmbedding");
34
+ const parser_1 = require("./conversation/parser");
35
+ const writer_1 = require("./conversation/writer");
36
+ const sessionRouter_1 = require("./router/sessionRouter");
37
+ const digests_1 = require("./digests/digests");
38
+ /**
39
+ * `serving`: the process is the long-running service (serve, mcp), the only one that may set an unterminated ledger tail
40
+ * aside at start-up. Every other command refuses such a tail and leaves the ledger alone (replayLedger).
41
+ */
42
+ async function setup({ serving = false } = {}) {
43
+ const config = (0, config_1.loadConfig)();
44
+ // D1 staleness guard (RENOR_VAULT_STALENESS=off|warn|fatal, default warn):
45
+ // the indexer must never silently serve a stale vault checkout again. Local
46
+ // git metadata only — no network, no fetch, zero model calls.
47
+ const gate = (0, staleness_1.stalenessGate)(config.vaultPath);
48
+ gate.stderrLines.forEach((l) => console.error(l));
49
+ if (gate.fatal) {
50
+ console.error("[staleness] RENOR_VAULT_STALENESS=fatal — refusing to serve a stale vault");
51
+ process.exit(1);
52
+ }
53
+ const db = (0, client_1.openIndexDb)(config.dbPath, (0, lifecycle_1.resolveLedgerPath)(config.vaultPath, config.memoryLedger));
54
+ (0, schema_1.runMigrations)(db);
55
+ // B7: startup MUST await a validated ledger replay before readiness. Replay
56
+ // failures block startup (fail closed) instead of being swallowed in a
57
+ // background promise — a corrupt ledger must never serve stale/absent facts.
58
+ try {
59
+ const replayed = await (0, lifecycle_1.replayLedger)(db, config.vaultPath, config.memoryLedger, { setTornTailAside: serving });
60
+ if (replayed > 0)
61
+ console.error(`[memory] replayed ${replayed} ledger event(s)`);
62
+ }
63
+ catch (err) {
64
+ console.error(`[memory] ledger replay failed:`, err instanceof Error ? err.message : err);
65
+ db.close();
66
+ process.exit(1);
67
+ }
68
+ let provider;
69
+ if (config.embeddingProvider === "local") {
70
+ provider = new local_1.LocalEmbeddingProvider(config.embeddingModel);
71
+ }
72
+ else if (config.embeddingProvider === "eval-hash") {
73
+ provider = new hashEmbedding_1.EvalHashEmbeddingProvider();
74
+ }
75
+ else if (config.embeddingProvider === "openai") {
76
+ provider = new openai_1.OpenAIEmbeddingProvider(config.openaiApiKey.split(',').map(k => k.trim()).filter(Boolean), config.embeddingModel, config.embeddingBaseURL, config.embeddingDims);
77
+ }
78
+ if (!provider)
79
+ console.error('[embeddings] Semantic search is off; set EMBEDDING_PROVIDER=openai to turn it on.');
80
+ const vectorIndex = provider ? new vectorIndex_1.VectorIndex(db, provider.dimensions) : undefined;
81
+ vectorIndex?.initTable();
82
+ const { factExtractor, numericFactExtractor, eventExtractor, preferenceExtractor } = (0, cli_extractors_1.buildWritePathExtractors)(config);
83
+ return { config, db, provider, vectorIndex, factExtractor, numericFactExtractor, eventExtractor, preferenceExtractor };
84
+ }
85
+ /** Flags that write structured rows, and only the commands that extract them. */
86
+ function armedWithoutProvider(extractors) {
87
+ return [
88
+ (0, typedFacts_1.typedFactsEnabled)() && !extractors.numericFactExtractor ? 'RENOR_TYPED_FACTS' : '',
89
+ (0, eventExtractor_1.chronicleEnabled)() && !extractors.eventExtractor ? 'RENOR_CHRONICLE' : '',
90
+ (0, preferenceExtractor_1.prefsEnabled)() && !extractors.preferenceExtractor ? 'RENOR_PREFS' : '',
91
+ ].filter(Boolean);
92
+ }
93
+ function refuseArmedWithoutProvider(db, flags) {
94
+ if (flags.length === 0)
95
+ return;
96
+ // A flag without a provider used to index cleanly and write nothing.
97
+ // The eval runner treats a clean index as success, so extraction commands
98
+ // have to fail here rather than look like an empty extraction.
99
+ // search, serve, and mcp do not extract; they must not exit on these flags.
100
+ console.error(`[extraction] armed without a provider: ${flags.join(', ')}`);
101
+ db.close();
102
+ process.exit(1);
103
+ }
104
+ /**
105
+ * RENOR_SESSION_ROUTER armed with no model to write the summaries it routes on. Same contract as the extraction
106
+ * flags above: an armed flag that cannot run must fail the index, or a benchmark arm indexes cleanly and then
107
+ * measures the control twice.
108
+ */
109
+ function refuseRouterWithoutProvider(db, router) {
110
+ if (!router || router.summaryChat)
111
+ return;
112
+ console.error('[router] armed without a provider: RENOR_SESSION_ROUTER needs RENOR_ROUTER_API_KEY(S), RENOR_EVIDENCE_API_KEY(S) or an Ollama Cloud key');
113
+ db.close();
114
+ process.exit(1);
115
+ }
116
+ /** Index-time summaries for the session router. Returns how many notes were left without one (an index error). */
117
+ async function buildRouterSummaries(db, config, router) {
118
+ const stats = await (0, sessionRouter_1.summarizeForRouter)({ db, config, options: router });
119
+ console.log(`[router] summaries ${JSON.stringify(stats)}`);
120
+ if (stats.failed > 0) {
121
+ console.error(`[router] ${stats.failed} note(s) left without a summary; search will not route until they have one${stats.error ? ` (${stats.error})` : ''}`);
122
+ }
123
+ return stats.failed;
124
+ }
125
+ /**
126
+ * RENOR_DIGESTS armed with no model to write the digests it reads. Same contract as the router: an armed flag that
127
+ * cannot run must fail the index, or a benchmark arm indexes cleanly and then measures the control twice. A cache-only
128
+ * run (RENOR_DIGEST_CACHE_ONLY) calls no model on purpose and needs none.
129
+ */
130
+ function refuseDigestsWithoutProvider(db, digests) {
131
+ if (!digests || digests.chat || digests.cacheOnly)
132
+ return;
133
+ // The digest slot is the only provider notes are sent to: another provider's key is never borrowed (resolveDigestProvider).
134
+ console.error('[digests] armed without a provider: RENOR_DIGESTS needs RENOR_DIGEST_BASE_URL and RENOR_DIGEST_API_KEY(S); it never uses the evidence provider or an Ollama Cloud key instead, so nothing was sent');
135
+ db.close();
136
+ process.exit(1);
137
+ }
138
+ /**
139
+ * RENOR_DIGESTS armed with semantic search off (EMBEDDING_PROVIDER omitted). Digests are written and looked up by their
140
+ * embedding, cache-only runs included, so with no embedding provider the lane would index and search as if no digest existed.
141
+ * Same contract as above: an armed flag that cannot run fails the command instead of quietly measuring the control.
142
+ */
143
+ function refuseDigestsWithoutEmbeddings(db, digests, provider) {
144
+ if (!digests || provider)
145
+ return;
146
+ console.error('[digests] armed without an embedding provider: RENOR_DIGESTS writes and looks up digests by embedding, so it needs EMBEDDING_PROVIDER (for example openai); semantic search is off, so no digest was built or read');
147
+ db.close();
148
+ process.exit(1);
149
+ }
150
+ /** Index-time digests. Returns how many notes were left without one (an index error). */
151
+ async function buildDigests(db, config, digests, provider) {
152
+ const stats = await (0, digests_1.digestIndexedNotes)({ db, config, options: digests, provider });
153
+ console.log(`[digests] ${JSON.stringify(stats)}`);
154
+ if (stats.failed > 0) {
155
+ console.error(`[digests] ${stats.failed} note(s) left without a digest; search will not read them until they have one${stats.error ? ` (${stats.error})` : ''}`);
156
+ }
157
+ return stats.failed;
158
+ }
159
+ /**
160
+ * The Markdown files the walk could not name because a file or folder name holds bytes that are
161
+ * not UTF-8. They are not errors — no note failed — but silently dropping them would make an
162
+ * index that is short of the vault look complete, so they are named here with the fix. A note over
163
+ * MAX_NOTE_BYTES is the same: never read, so no failure, but named.
164
+ */
165
+ function printNotIndexed(names, labelWidth, tooLarge = []) {
166
+ if (tooLarge.length > 0) {
167
+ console.log(` ${'not indexed'.padEnd(labelWidth)}: ${tooLarge.length} Markdown file(s) over ${pipeline_1.MAX_NOTE_BYTES / (1024 * 1024)} MiB`
168
+ + ' (not read); split them or move them out of the memory folder, then index again');
169
+ for (const name of tooLarge.slice(0, 10))
170
+ console.log(` ! ${name}`);
171
+ if (tooLarge.length > 10)
172
+ console.log(` ! …and ${tooLarge.length - 10} more`);
173
+ }
174
+ if (names.length === 0)
175
+ return;
176
+ console.log(` ${'not indexed'.padEnd(labelWidth)}: ${names.length} Markdown file(s) whose file or folder name is not valid UTF-8`
177
+ + ' (bad bytes are shown as \\xNN); rename them to UTF-8 names, then index again');
178
+ for (const name of names.slice(0, 10))
179
+ console.log(` ! ${name}`);
180
+ if (names.length > 10)
181
+ console.log(` ! …and ${names.length - 10} more`);
182
+ }
183
+ const program = new commander_1.Command().name("renor").version("1.0.0");
184
+ program
185
+ .command("index")
186
+ .description("Index full vault")
187
+ .action(async () => {
188
+ const { config, db, provider, vectorIndex, factExtractor, numericFactExtractor, eventExtractor, preferenceExtractor } = await setup();
189
+ refuseArmedWithoutProvider(db, armedWithoutProvider({
190
+ numericFactExtractor, eventExtractor, preferenceExtractor,
191
+ }));
192
+ const router = (0, sessionRouter_1.resolveSessionRouterOptions)(config);
193
+ refuseRouterWithoutProvider(db, router);
194
+ const digests = (0, digests_1.resolveDigestOptions)(config);
195
+ refuseDigestsWithoutProvider(db, digests);
196
+ refuseDigestsWithoutEmbeddings(db, digests, provider);
197
+ console.log(`Indexing: ${config.vaultPath}`);
198
+ const stats = await (0, pipeline_1.indexVault)(db, config.vaultPath, {
199
+ embeddingProvider: provider,
200
+ vectorIndex,
201
+ chunkOptions: {
202
+ maxTokens: config.chunkMaxTokens,
203
+ overlapTokens: config.chunkOverlapTokens,
204
+ },
205
+ factExtractor,
206
+ numericFactExtractor,
207
+ eventExtractor,
208
+ preferenceExtractor,
209
+ }, config.indexConcurrency);
210
+ console.log(`Done. ${stats.indexed}/${stats.total} indexed, ${stats.errors} errors.`);
211
+ printNotIndexed(stats.skippedNonUtf8Names, 15, stats.skippedTooLarge);
212
+ const unsummarized = router ? await buildRouterSummaries(db, config, router) : 0;
213
+ const undigested = digests && provider ? await buildDigests(db, config, digests, provider) : 0;
214
+ db.close();
215
+ // #188: a partial index is not a success. The completion line reports the
216
+ // error count and the process status must agree with it, or automation
217
+ // (the eval runner included) accepts an incomplete index. Never a blanket
218
+ // catch: the count is the failure, and a clean run still exits 0.
219
+ // A note the armed session router could not summarise is the same kind of failure.
220
+ if (stats.errors > 0 || unsummarized > 0 || undigested > 0)
221
+ process.exit(1);
222
+ });
223
+ program
224
+ .command("search <query>")
225
+ .description("Semantic search")
226
+ .option("-k, --top-k <n>", "results", "20")
227
+ .option("--json", "machine-readable JSON output (full hit text; the benchmark client transport)")
228
+ .action(async (query, opts) => {
229
+ const { config, db, provider, vectorIndex } = await setup();
230
+ // D1: the machine-readable transport carries the staleness verdict so a
231
+ // stale-serving index is visible to the CLIENT, not only in stderr — but only
232
+ // when the operator EXPLICITLY armed the guard (RENOR_VAULT_STALENESS=warn|fatal).
233
+ // Unset must stay byte-identical to the frozen `search --json` contract even on a
234
+ // stale vault: the contract test's plain-folder fixture cannot reach that case, so
235
+ // this is pinned at the CLI surface by tests/context/staleness-unarmed-contract.test.ts
236
+ // (see stalenessJsonField).
237
+ const vaultStaleness = (0, staleness_1.stalenessJsonField)((0, staleness_1.stalenessGate)(config.vaultPath).staleness);
238
+ // The ledger is the engine's own record, not a memory: a search returns neither its header nor the markup of its records.
239
+ const isLedger = (0, ledgerPresentation_1.ledgerNoteMatcher)({ memoryDir: config.vaultPath, ledgerRelPath: config.memoryLedger ?? lifecycle_1.DEFAULT_LEDGER_REL });
240
+ // RENOR_READER_EVENTS reads the question's vector for its lines, only for an order or a date question: nothing else changes.
241
+ const probe = (0, readerEvents_1.readerEventsEnabled)() && (0, readerEvents_1.isEventQuestion)(query) ? {} : undefined;
242
+ const retrieved = await (0, orchestrator_1.retrieveContext)(db, vectorIndex, provider, query, Number(opts.topK), config.priorityPaths, {}, { isLedgerNote: isLedger, ...(probe ? { probe } : {}) });
243
+ // Session router (RENOR_SESSION_ROUTER, default off): null when off, and then nothing here changes.
244
+ const routed = await (0, sessionRouter_1.routeSearch)({ db, config, query, retrieved });
245
+ const evidence = (0, options_1.resolveEvidenceToolOptions)(config);
246
+ const result = await (0, attach_1.attachSearchEvidence)({
247
+ query,
248
+ hits: routed ? routed.hits : retrieved.hits,
249
+ vaultPath: config.vaultPath,
250
+ options: evidence,
251
+ extractor: (0, attach_1.searchEvidenceEnabled)() ? (0, options_1.createEvidenceExtractor)(evidence) : null,
252
+ });
253
+ // Write-time digests (RENOR_DIGESTS, default off): null when off, and then nothing here changes. No model call.
254
+ const digestOptions = (0, digests_1.resolveDigestOptions)(config);
255
+ refuseDigestsWithoutEmbeddings(db, digestOptions, provider);
256
+ const digested = digestOptions && provider
257
+ ? await (0, digests_1.attachDigest)({ db, query, hits: result.hits, options: digestOptions, provider, isLedgerNote: isLedger })
258
+ : null;
259
+ // The evidence pass read the sources as stored; what is printed is what the user said.
260
+ // Laid out for the model that reads it (RENOR_READER_LAYOUT; readerLayout.ts): the hits of a session come as one, and its time phrases are resolved. RENOR_READER_EVENTS adds event lines above them for an order or a date question.
261
+ // A query that asks to be advised keeps the page as it was when RENOR_READER_LAYOUT_SKIP_ADVICE is on.
262
+ // RENOR_READER_ADVICE_LINES (default off): for a query that asks to be advised, the user's own matching statements come first (adviceLines.ts).
263
+ const presented = (0, ledgerPresentation_1.presentLedgerEvents)(digested ? digested.hits : result.hits, isLedger);
264
+ const notTable = (p) => !isLedger(p) && p !== attach_1.EVIDENCE_HIT_PATH && p !== digests_1.DIGEST_HIT_PATH;
265
+ let conversationNote;
266
+ const hits = (0, adviceLines_1.withAdviceLines)((0, readerLayout_1.readerLayoutEnabledFor)(query)
267
+ ? (0, readerLayout_1.layoutHitsForReader)(presented, { rankedBy: retrieved.rankedBy, isLedger: (p) => isLedger(p) || p === attach_1.EVIDENCE_HIT_PATH || p === digests_1.DIGEST_HIT_PATH, isConversation: (0, readerLayout_1.conversationNoteMatcher)(db), query, turnsOf: (0, readerEvents_1.readerEventsEnabled)() ? (0, readerEvents_1.noteTurnsLoader)(db) : undefined, scoreTurns: probe?.queryVector && provider && vectorIndex && (0, readerEvents_1.vectorsMeanSomething)(provider.identity?.kind) ? (0, readerEvents_1.turnScorer)(vectorIndex, probe.queryVector) : undefined })
268
+ : presented, { query, turnsOf: () => (0, readerEvents_1.noteTurnsLoader)(db), isSource: (h) => (h.chunkId ?? 0) > 0 && h.notePath !== '' && notTable(h.notePath) && (conversationNote ??= (0, readerLayout_1.conversationNoteMatcher)(db))(h.notePath) });
269
+ if (opts.json) {
270
+ // Sole stdout content: machine-readable hits with FULL text. This is the
271
+ // public CLI contract consumed by external clients (authoritative
272
+ // benchmark runner); it must not be mixed with prose output.
273
+ // C0 composer (ADR-0003 §5): RENOR_STRUCTURED_CONTEXT (default off) adds
274
+ // the composed bounded context alongside the unchanged hits array.
275
+ const composed = (0, composer_1.structuredContextEnabled)()
276
+ ? (0, composer_1.composeQueryContextFromDb)(db, { query, hits: hits.filter((h) => h.notePath !== attach_1.EVIDENCE_HIT_PATH), evidence: result.evidence })
277
+ : undefined;
278
+ // A note's path is printed as the hosted tools print it (clientPath): relative to the vault, so the reply names
279
+ // neither the host's filesystem layout nor its temporary directory, and its size does not depend on where the
280
+ // vault lives. The notes were found, and the composer joined its rows to them, by the stored path: only what is
281
+ // printed below changes, and the evidence and digest tables' own paths (they are no files) stay what clients match
282
+ // them by.
283
+ const shown = (stored) => (stored === attach_1.EVIDENCE_HIT_PATH || stored === digests_1.DIGEST_HIT_PATH ? stored : (0, engine_1.clientPath)(config.vaultPath, stored));
284
+ const shownHits = hits.map((h) => ({ ...h, notePath: shown(h.notePath) }));
285
+ const shownRouter = routed?.report.routed
286
+ ? { ...routed.report, routed: routed.report.routed.map((r) => ({ ...r, notePath: shown(r.notePath) })) }
287
+ : routed?.report;
288
+ const shownDigest = digested
289
+ ? { ...digested.report, items: digested.report.items.map((i) => ({ ...i, notePath: shown(i.notePath) })) }
290
+ : undefined;
291
+ const shownEvidence = result.evidence.sessions
292
+ ? { ...result.evidence, sessions: result.evidence.sessions.map((s) => ({ ...s, stem: shown(s.stem) })) }
293
+ : result.evidence;
294
+ // LANE 3 read-time honesty (RENOR_READ_HONESTY, default off): adds
295
+ // provenance + contradiction + abstention alongside the unchanged hits
296
+ // array. Nothing is filtered, hidden or reordered — when the evidence
297
+ // does not cover the question the read SAYS SO instead of presenting
298
+ // weak hits as an answer.
299
+ const honesty = (0, readHonesty_1.readHonestyEnabled)()
300
+ ? (0, readHonesty_1.readHonestyForItems)({ db, query, items: shownHits, ledgerRelPath: config.memoryLedger ?? lifecycle_1.DEFAULT_LEDGER_REL })
301
+ : undefined;
302
+ console.log(JSON.stringify({
303
+ hits: shownHits.map((h) => ({
304
+ notePath: h.notePath,
305
+ score: h.score,
306
+ reason: h.reason,
307
+ text: h.text,
308
+ ...(h.session_date ? { session_date: h.session_date } : {}),
309
+ ...(honesty ? { provenance: (0, readHonesty_1.noteProvenance)(h.notePath, h.session_date ?? null) } : {}),
310
+ })),
311
+ ...(composed ? { composedContext: composed } : {}),
312
+ ...(honesty ? { readHonesty: honesty } : {}),
313
+ ...(shownRouter ? { router: shownRouter } : {}),
314
+ ...(shownDigest ? { digest: shownDigest } : {}),
315
+ ...vaultStaleness,
316
+ // What the evidence pass did, in the shape search_memory returns, so a
317
+ // failed extraction is said instead of being a silently missing table
318
+ // (#222). RENOR_SEARCH_EVIDENCE=0 keeps the frozen envelope.
319
+ ...(result.evidence.reason === "flag-off" ? {} : { evidence: (0, attach_2.productEvidenceReport)(shownEvidence) }),
320
+ }));
321
+ }
322
+ else {
323
+ if (routed) {
324
+ console.log(`[router] ${routed.report.status}${routed.report.reason ? ` (${routed.report.reason})` : ''}`);
325
+ }
326
+ if ((0, composer_1.structuredContextEnabled)()) {
327
+ const composed = (0, composer_1.composeQueryContextFromDb)(db, { query, hits, evidence: result.evidence });
328
+ console.log(composed.text);
329
+ console.log("");
330
+ }
331
+ hits.forEach((h) => {
332
+ console.log(`\n[${h.score.toFixed(3)}] ${(0, engine_1.clientPath)(config.vaultPath, h.notePath)} (${h.reason})`);
333
+ console.log(h.text.substring(0, 200));
334
+ });
335
+ }
336
+ db.close();
337
+ });
338
+ program
339
+ .command("watch")
340
+ .description("Watch vault for changes")
341
+ .action(async () => {
342
+ const { config, db, provider, vectorIndex, factExtractor, numericFactExtractor, eventExtractor, preferenceExtractor } = await setup();
343
+ refuseArmedWithoutProvider(db, armedWithoutProvider({
344
+ numericFactExtractor, eventExtractor, preferenceExtractor,
345
+ }));
346
+ console.log(`Watching: ${config.vaultPath}`);
347
+ (0, watcher_1.startWatcher)(config.vaultPath, db, {
348
+ embeddingProvider: provider,
349
+ vectorIndex,
350
+ factExtractor,
351
+ numericFactExtractor,
352
+ eventExtractor,
353
+ preferenceExtractor,
354
+ });
355
+ });
356
+ program
357
+ .command("serve")
358
+ .description("Start HTTP API")
359
+ .option("-p, --port <n>", "port", "3456")
360
+ .action(async (opts) => {
361
+ // Checked before anything is opened: a missing token must not leave a database or an index half-started.
362
+ let apiToken;
363
+ try {
364
+ apiToken = (0, apiAuth_1.requireApiToken)();
365
+ }
366
+ catch (err) {
367
+ console.error(`[serve] ${err.message}`);
368
+ process.exit(1);
369
+ }
370
+ const { config, db, provider, vectorIndex } = await setup({ serving: true });
371
+ const app = (0, server_1.createServer)(db, vectorIndex, provider, config.vaultPath, config.priorityPaths, config.memoryDir, config.memoryLedger, { apiToken });
372
+ app.listen(Number(opts.port), "127.0.0.1", () => console.log(`Listening on http://127.0.0.1:${opts.port}`));
373
+ });
374
+ program
375
+ .command("rebuild")
376
+ .description("Clear derived data and reindex from scratch")
377
+ .action(async () => {
378
+ const { config, db, provider, vectorIndex, factExtractor, numericFactExtractor, eventExtractor, preferenceExtractor } = await setup();
379
+ refuseArmedWithoutProvider(db, armedWithoutProvider({
380
+ numericFactExtractor, eventExtractor, preferenceExtractor,
381
+ }));
382
+ const router = (0, sessionRouter_1.resolveSessionRouterOptions)(config);
383
+ refuseRouterWithoutProvider(db, router);
384
+ const digests = (0, digests_1.resolveDigestOptions)(config);
385
+ refuseDigestsWithoutProvider(db, digests);
386
+ refuseDigestsWithoutEmbeddings(db, digests, provider);
387
+ let rebuildErrors = 0;
388
+ try {
389
+ // Clear vector index — drop and recreate
390
+ db.prepare("DROP TABLE IF EXISTS chunk_embeddings").run();
391
+ vectorIndex?.initTable(); // recreates it
392
+ // Clear SQL derived tables atomically
393
+ db.transaction(() => {
394
+ ["facts", "relations", "entities", "chunks", "notes", "applied_ledger_events"].forEach((t) => db.prepare(`DELETE FROM ${t}`).run());
395
+ })();
396
+ // The ledger is the source of truth: after the derived tables are wiped,
397
+ // replay MUST run again (awaited) or the rebuild would silently drop
398
+ // every lifecycle fact until the next restart. Errors block the rebuild.
399
+ const replayed = await (0, lifecycle_1.replayLedger)(db, config.vaultPath, config.memoryLedger);
400
+ console.log(`Cleared. Replayed ${replayed} ledger event(s). Reindexing...`);
401
+ const rebuildStats = await (0, pipeline_1.indexVault)(db, config.vaultPath, {
402
+ embeddingProvider: provider,
403
+ vectorIndex,
404
+ chunkOptions: {
405
+ maxTokens: config.chunkMaxTokens,
406
+ overlapTokens: config.chunkOverlapTokens,
407
+ },
408
+ factExtractor,
409
+ numericFactExtractor,
410
+ eventExtractor,
411
+ preferenceExtractor,
412
+ }, config.indexConcurrency);
413
+ console.log(`Rebuild complete. ${rebuildStats.indexed}/${rebuildStats.total} indexed, ${rebuildStats.errors} errors.`);
414
+ printNotIndexed(rebuildStats.skippedNonUtf8Names, 17, rebuildStats.skippedTooLarge);
415
+ rebuildErrors = rebuildStats.errors + (router ? await buildRouterSummaries(db, config, router) : 0)
416
+ + (digests && provider ? await buildDigests(db, config, digests, provider) : 0);
417
+ }
418
+ catch (err) {
419
+ console.error(`[memory] rebuild failed:`, err instanceof Error ? err.message : err);
420
+ db.close();
421
+ process.exit(1);
422
+ }
423
+ db.close();
424
+ // Same failure contract as `index` (#188): the rebuild reports its error
425
+ // count, and the exit status must carry it. A wipe-and-reindex that left
426
+ // notes behind is not a successful rebuild.
427
+ if (rebuildErrors > 0)
428
+ process.exit(1);
429
+ });
430
+ program
431
+ .command("mcp")
432
+ .description("Start MCP server (stdio transport) for agent connectivity")
433
+ .action(async () => {
434
+ const { config, db, provider, vectorIndex } = await setup({ serving: true });
435
+ // stderr only: stdout is the JSON-RPC stream. Said when the folder is the default, so a person who set nothing can find it.
436
+ if (!process.env.VAULT_PATH)
437
+ process.stderr.write(`Renor memory: ${config.vaultPath}\n`);
438
+ try {
439
+ await (0, server_2.startMcpServer)(db, vectorIndex, provider, config);
440
+ }
441
+ catch (err) {
442
+ // A refused startup is an operator-facing answer, not a crash: another live `renor-engine mcp`
443
+ // already holds this connection's name (memory/agentLease.ts). Say it on one line and exit
444
+ // non-zero, so a client that launches the server reads a stderr it can act on. stdout
445
+ // stays empty either way, so a JSON-RPC stream is never polluted.
446
+ process.stderr.write(`renor-engine mcp: ${err.message}\n`);
447
+ process.exit(1);
448
+ }
449
+ });
450
+ program
451
+ .command("log-conversation")
452
+ .description("Log a Claude Code session to Obsidian vault (invoked by Stop hook)")
453
+ .action(async () => {
454
+ const vaultPath = process.env.VAULT_PATH;
455
+ const conversationsDir = process.env.CONVERSATIONS_DIR ?? "Claude Code/Conversations";
456
+ if (!vaultPath) {
457
+ process.stderr.write("log-conversation: VAULT_PATH not set\n");
458
+ process.exit(0);
459
+ }
460
+ // Читаем JSON-пейлоад Stop hook из stdin
461
+ const chunks = [];
462
+ for await (const chunk of process.stdin)
463
+ chunks.push(chunk);
464
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
465
+ let sessionId;
466
+ let cwd;
467
+ try {
468
+ const payload = JSON.parse(raw);
469
+ sessionId = payload.session_id;
470
+ cwd = payload.cwd;
471
+ }
472
+ catch {
473
+ process.stderr.write("log-conversation: invalid JSON on stdin\n");
474
+ process.exit(0);
475
+ }
476
+ if (!sessionId || !cwd) {
477
+ process.stderr.write("log-conversation: missing session_id or cwd in payload\n");
478
+ process.exit(0);
479
+ }
480
+ try {
481
+ const jsonlPath = (0, parser_1.resolveJSONLPath)(sessionId, cwd);
482
+ const conv = await (0, parser_1.parseConversation)(jsonlPath, sessionId, cwd);
483
+ const filePath = await (0, writer_1.writeConversation)(vaultPath, conversationsDir, conv);
484
+ process.stdout.write(`Conversation saved: ${filePath}\n`);
485
+ }
486
+ catch (err) {
487
+ process.stderr.write(`log-conversation: ${err}\n`);
488
+ process.exit(0);
489
+ }
490
+ });
491
+ program.parse();
package/dist/config.js ADDED
@@ -0,0 +1,65 @@
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.defaultVaultPath = defaultVaultPath;
7
+ exports.loadConfig = loadConfig;
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ /** The notes folder when VAULT_PATH is unset or empty. */
11
+ function defaultVaultPath() {
12
+ return node_path_1.default.join(node_os_1.default.homedir(), '.renor');
13
+ }
14
+ function loadConfig() {
15
+ // No setting is needed for a first run (`npx -y renor-engine mcp`): notes live in ~/.renor. A hidden name, so it can never be
16
+ // an existing folder of the user's (a clone named renor, say) that the first save would write notes and an index into.
17
+ const vaultPath = process.env.VAULT_PATH || defaultVaultPath();
18
+ const rawProvider = process.env.EMBEDDING_PROVIDER ?? 'none';
19
+ if (rawProvider !== 'none' && rawProvider !== 'openai' && rawProvider !== 'local' && rawProvider !== 'eval-hash') {
20
+ throw new Error(`Invalid EMBEDDING_PROVIDER "${rawProvider}": must be "none", "openai", "local", or "eval-hash"`);
21
+ }
22
+ // eval-hash: deterministic lexical embeddings for evaluation smoke/contract
23
+ // runs through the public CLI surface (offline, no model download, no
24
+ // semantic-quality claim). Same provider CI already uses for RenorBench smoke.
25
+ const embeddingProvider = rawProvider;
26
+ if (embeddingProvider === 'openai') {
27
+ const key = process.env.OPENAI_API_KEY;
28
+ if (!key || key.trim() === '') {
29
+ throw new Error('OPENAI_API_KEY is required when EMBEDDING_PROVIDER is "openai"');
30
+ }
31
+ }
32
+ const chunkMaxTokens = Number(process.env.CHUNK_MAX_TOKENS ?? 400);
33
+ if (!Number.isFinite(chunkMaxTokens) || chunkMaxTokens <= 0) {
34
+ throw new Error('CHUNK_MAX_TOKENS must be a positive number');
35
+ }
36
+ const chunkOverlapTokens = Number(process.env.CHUNK_OVERLAP_TOKENS ?? 50);
37
+ if (!Number.isFinite(chunkOverlapTokens) || chunkOverlapTokens < 0) {
38
+ throw new Error('CHUNK_OVERLAP_TOKENS must be a non-negative number');
39
+ }
40
+ if (chunkOverlapTokens >= chunkMaxTokens) {
41
+ throw new Error('CHUNK_OVERLAP_TOKENS must be less than CHUNK_MAX_TOKENS');
42
+ }
43
+ const indexConcurrency = Number(process.env.INDEX_CONCURRENCY ?? 5);
44
+ if (!Number.isInteger(indexConcurrency) || indexConcurrency < 1 || indexConcurrency > 20) {
45
+ throw new Error('INDEX_CONCURRENCY must be an integer between 1 and 20');
46
+ }
47
+ const rawPriorityPaths = process.env.PRIORITY_PATHS ?? '';
48
+ const priorityPaths = rawPriorityPaths ? rawPriorityPaths.split(',').map(p => p.trim()).filter(Boolean) : [];
49
+ return {
50
+ vaultPath,
51
+ dbPath: process.env.DB_PATH ?? `${vaultPath}/.semantic-memory/index.db`,
52
+ embeddingProvider,
53
+ openaiApiKey: process.env.OPENAI_API_KEY,
54
+ embeddingModel: process.env.EMBEDDING_MODEL,
55
+ embeddingBaseURL: process.env.EMBEDDING_BASE_URL,
56
+ embeddingDims: process.env.EMBEDDING_DIMS ? parseInt(process.env.EMBEDDING_DIMS, 10) : undefined,
57
+ chunkMaxTokens,
58
+ chunkOverlapTokens,
59
+ priorityPaths,
60
+ memoryDir: process.env.MEMORY_DIR ?? 'Memory/Daily',
61
+ memoryLedger: process.env.MEMORY_LEDGER ?? 'System/Memory Events Ledger.md',
62
+ llmExtraction: process.env.LLM_EXTRACTION === 'true',
63
+ indexConcurrency,
64
+ };
65
+ }