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,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OLLAMA_KEY_ENV_NAMES = void 0;
4
+ exports.splitKeyList = splitKeyList;
5
+ exports.collectOllamaKeys = collectOllamaKeys;
6
+ exports.isOllamaCloudEndpoint = isOllamaCloudEndpoint;
7
+ exports.ollamaKeysForEndpoint = ollamaKeysForEndpoint;
8
+ const ollamaChat_1 = require("./ollamaChat");
9
+ /**
10
+ * ONE source of truth for the Ollama Cloud key pool.
11
+ *
12
+ * WHY THIS FILE EXISTS
13
+ * Two modules read the same env namespace and they disagreed. The write-path
14
+ * extractor (`extraction/extractionClient.ts`) resolved SIX names; the
15
+ * evidence second pass (`evidence/options.ts`) resolved THREE, and the three
16
+ * it named were the first three of the other list. On 2026-09-30 that drift
17
+ * cost a 500-case product run: `OLLAMA_CLOUD_KEY_1..3` had all hit their
18
+ * MONTHLY ceiling (429) while `OLLAMA_CLOUD_KEY_4`, `OLLAMA_MAX_KEY` and
19
+ * `OLLAMA_API_KEY` were healthy, so the evidence extractor exhausted its pool
20
+ * and — because `attachSearchEvidence` is deliberately fail-open — the last 77
21
+ * of 500 cases were answered with the evidence layer silently absent. The run
22
+ * manifest reported full coverage and zero `evid*` keys: a run that lost its
23
+ * evidence layer for 15% of cases was byte-indistinguishable from one where it
24
+ * held. See issue #222.
25
+ *
26
+ * The defect was not a missing key. It was two hand-maintained copies of one
27
+ * list. Keys are read by env-var NAME; values are never logged here.
28
+ */
29
+ exports.OLLAMA_KEY_ENV_NAMES = [
30
+ 'OLLAMA_CLOUD_KEY_1',
31
+ 'OLLAMA_CLOUD_KEY_2',
32
+ 'OLLAMA_CLOUD_KEY_3',
33
+ 'OLLAMA_CLOUD_KEY_4',
34
+ 'OLLAMA_MAX_KEY',
35
+ 'OLLAMA_API_KEY',
36
+ ];
37
+ /** Comma-separated env value -> trimmed, non-empty keys. */
38
+ function splitKeyList(value) {
39
+ return (value ?? '').split(',').map(key => key.trim()).filter(Boolean);
40
+ }
41
+ /**
42
+ * Collect every configured Ollama key, in the fixed name order above, together
43
+ * with the names that supplied them. `names` is safe to report; `keys` is not.
44
+ */
45
+ function collectOllamaKeys(env = process.env) {
46
+ const keys = [];
47
+ const names = [];
48
+ for (const name of exports.OLLAMA_KEY_ENV_NAMES) {
49
+ const found = splitKeyList(env[name]);
50
+ if (found.length > 0) {
51
+ keys.push(...found);
52
+ names.push(name);
53
+ }
54
+ }
55
+ return { keys, names };
56
+ }
57
+ /**
58
+ * Whether `baseUrl` is Ollama Cloud, the only issuer these keys belong to:
59
+ * https on ollama.com or a subdomain of it. Blank means the transport default,
60
+ * which is Ollama Cloud. A look-alike host, plain http or an unparseable URL is
61
+ * not.
62
+ */
63
+ function isOllamaCloudEndpoint(baseUrl) {
64
+ let url;
65
+ try {
66
+ url = new URL(baseUrl?.trim() || ollamaChat_1.DEFAULT_CHAT_BASE_URL);
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ const host = url.hostname.toLowerCase();
72
+ return url.protocol === 'https:' && (host === 'ollama.com' || host.endsWith('.ollama.com'));
73
+ }
74
+ /**
75
+ * The Ollama pool for a client that will call `baseUrl`: every key when that
76
+ * is Ollama Cloud, none otherwise.
77
+ *
78
+ * The evidence client used to append this pool to whatever endpoint
79
+ * RENOR_EVIDENCE_BASE_URL named, so pointing the extractor at another provider
80
+ * sent every leftover OLLAMA_* key there too: a credential leak, and a 401 on
81
+ * each search whose turn in the rotation fell on an Ollama key.
82
+ */
83
+ function ollamaKeysForEndpoint(baseUrl, env = process.env) {
84
+ return isOllamaCloudEndpoint(baseUrl) ? collectOllamaKeys(env) : { keys: [], names: [] };
85
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CHAT_BASE_URL = void 0;
4
+ exports.createOllamaChat = createOllamaChat;
5
+ /** The endpoint called when no base URL is given: Ollama Cloud. */
6
+ exports.DEFAULT_CHAT_BASE_URL = 'https://ollama.com/v1';
7
+ function createOllamaChat(opts) {
8
+ const baseUrl = (opts.baseUrl ?? exports.DEFAULT_CHAT_BASE_URL).replace(/\/+$/, '');
9
+ const keys = opts.keys.filter(k => k && k.trim());
10
+ if (keys.length === 0)
11
+ throw new Error('createOllamaChat requires at least one API key');
12
+ const timeoutMs = opts.timeoutMs ?? 180_000;
13
+ const maxRetries = opts.maxRetries ?? 3;
14
+ const backoffMs = opts.backoffMs ?? 1000;
15
+ let counter = 0;
16
+ return async (request) => {
17
+ const body = JSON.stringify({
18
+ model: request.model,
19
+ messages: request.messages,
20
+ temperature: request.temperature,
21
+ // Only when the caller asked for JSON mode. Evidence calls omit it.
22
+ ...(request.response_format ? { response_format: request.response_format } : {}),
23
+ // Only when the caller set a bound, so an unbounded request is the same bytes: the session router always sets
24
+ // a ceiling, the evidence pass only when the operator set one (RENOR_EVIDENCE_MAX_TOKENS /
25
+ // RENOR_EVIDENCE_REASONING_EFFORT), the extractors never.
26
+ ...(request.max_tokens !== undefined ? { max_tokens: request.max_tokens } : {}),
27
+ ...(request.reasoning_effort !== undefined ? { reasoning_effort: request.reasoning_effort } : {}),
28
+ });
29
+ let lastError;
30
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
31
+ opts.beforeAttempt?.();
32
+ let retryable = true;
33
+ const key = keys[counter++ % keys.length];
34
+ const started = Date.now();
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
37
+ try {
38
+ const resp = await fetch(`${baseUrl}/chat/completions`, {
39
+ method: 'POST',
40
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
41
+ body,
42
+ signal: controller.signal,
43
+ });
44
+ if (resp.status === 429 || resp.status >= 500) {
45
+ lastError = new Error(`extractor provider returned ${resp.status}`);
46
+ await resp.text().catch(() => '');
47
+ throw lastError;
48
+ }
49
+ if (!resp.ok) {
50
+ // 4xx other than 429 is the provider rejecting this request.
51
+ // Retrying it only spends the key pool and the backoff.
52
+ retryable = false;
53
+ lastError = new Error(`extractor provider returned ${resp.status}: ${(await resp.text().catch(() => '')).slice(0, 200)}`);
54
+ throw lastError;
55
+ }
56
+ const data = await resp.json();
57
+ const content = data.choices?.[0]?.message?.content;
58
+ const finishReason = data.choices?.[0]?.finish_reason;
59
+ if (typeof content !== 'string') {
60
+ // `length` here is a reasoning model that spent its whole budget thinking.
61
+ throw new Error(`extractor provider response missing choices[0].message.content${typeof finishReason === 'string' ? ` (finish_reason=${finishReason})` : ''}`);
62
+ }
63
+ return {
64
+ content,
65
+ promptTokens: data.usage?.prompt_tokens,
66
+ completionTokens: data.usage?.completion_tokens,
67
+ totalTokens: data.usage?.total_tokens,
68
+ latencyMs: Date.now() - started,
69
+ ...(typeof finishReason === 'string' ? { finishReason } : {}),
70
+ };
71
+ }
72
+ catch (err) {
73
+ lastError = err instanceof Error ? err : new Error(String(err));
74
+ if (!retryable || attempt >= maxRetries)
75
+ break;
76
+ await new Promise(resolve => setTimeout(resolve, 2 ** attempt * backoffMs));
77
+ }
78
+ finally {
79
+ clearTimeout(timer);
80
+ }
81
+ }
82
+ throw lastError ?? new Error('extractor call failed');
83
+ };
84
+ }
@@ -0,0 +1,165 @@
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.EvidenceDailyBudget = exports.EvidenceCapError = exports.EVIDENCE_DAILY_CAP_MAX = exports.EVIDENCE_DAILY_CAP_DEFAULT = exports.EVIDENCE_OPTIN_VALUE = void 0;
7
+ exports.resolveEvidenceOptin = resolveEvidenceOptin;
8
+ exports.readOptinKey = readOptinKey;
9
+ exports.optinEndpointAllowed = optinEndpointAllowed;
10
+ exports.utcDay = utcDay;
11
+ exports.dailyBudgetGate = dailyBudgetGate;
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const client_1 = require("../db/client");
14
+ const keyPool_1 = require("./keyPool");
15
+ /**
16
+ * The evidence extractor as a per-workspace opt-in (owner decision, 13 Oct 2026).
17
+ *
18
+ * A workspace runs the extractor on Ollama Cloud only when its own env file says so:
19
+ *
20
+ * RENOR_EVIDENCE_OPTIN=ollama the switch; unset or blank is off, and then nothing here applies
21
+ * RENOR_EVIDENCE_KEY_FILE=<path> the fleet key, by its path (a file, so the env file never holds the secret)
22
+ * RENOR_EVIDENCE_DAILY_CAP=<n> HTTP requests to the model per UTC day for this workspace (default 50)
23
+ *
24
+ * With the switch on, the key comes from the key file and nowhere else (no OLLAMA_* name, no
25
+ * RENOR_EVIDENCE_API_KEY), the endpoint is Ollama Cloud and the model is gemma4:31b; a base URL naming another
26
+ * issuer is refused rather than sent the Ollama key. The operator turns it on and off with
27
+ * scripts/alpha/set-tenant-evidence.sh. Values are never logged: only the key file's path and the reason a
28
+ * setting was refused.
29
+ */
30
+ exports.EVIDENCE_OPTIN_VALUE = 'ollama';
31
+ /**
32
+ * HTTP requests to the model a workspace may make per UTC day (a retry of a 429 or 5xx is one more). Sized for the shared account, not for the user: gemma4:31b
33
+ * reads about 9,300 tokens and writes about 190 per call (provider-terms report, 2026-10-03), so 50 calls are
34
+ * about 0.47M tokens, about 0.07 USD at list price, and 350 calls a week. Ollama publishes its limits as
35
+ * usage, not as calls, and does not state the weekly figure, so this stays well under any plausible share and
36
+ * is a setting (RENOR_EVIDENCE_DAILY_CAP, set per workspace by the ops command). A search answered from the
37
+ * cache costs none of it.
38
+ */
39
+ exports.EVIDENCE_DAILY_CAP_DEFAULT = 50;
40
+ exports.EVIDENCE_DAILY_CAP_MAX = 10_000;
41
+ /** null when the workspace has not opted in. A value that does not parse throws, like an invalid RENOR_EVIDENCE_MODE. */
42
+ function resolveEvidenceOptin(env = process.env) {
43
+ const raw = (env.RENOR_EVIDENCE_OPTIN ?? '').trim().toLowerCase();
44
+ if (raw === '')
45
+ return null;
46
+ if (raw !== exports.EVIDENCE_OPTIN_VALUE) {
47
+ throw new Error(`Invalid RENOR_EVIDENCE_OPTIN "${raw}": must be ${exports.EVIDENCE_OPTIN_VALUE} (or unset)`);
48
+ }
49
+ const keyFile = (env.RENOR_EVIDENCE_KEY_FILE ?? '').trim();
50
+ const capText = (env.RENOR_EVIDENCE_DAILY_CAP ?? '').trim();
51
+ let dailyCap = exports.EVIDENCE_DAILY_CAP_DEFAULT;
52
+ if (capText !== '') {
53
+ const value = Number(capText);
54
+ if (!/^[0-9]+$/.test(capText) || !Number.isSafeInteger(value) || value > exports.EVIDENCE_DAILY_CAP_MAX) {
55
+ throw new Error(`Invalid RENOR_EVIDENCE_DAILY_CAP "${capText}": must be a whole number from 0 to ${exports.EVIDENCE_DAILY_CAP_MAX}`);
56
+ }
57
+ dailyCap = value;
58
+ }
59
+ return { keyFile, dailyCap };
60
+ }
61
+ /**
62
+ * The key in the key file: one line, trimmed. Null (with one stderr line naming the path, never the content) when
63
+ * the setting names no file, the file is not a regular file, or it holds nothing usable.
64
+ */
65
+ function readOptinKey(keyFile) {
66
+ if (!keyFile.startsWith('/')) {
67
+ process.stderr.write('[evidence] opt-in is on but RENOR_EVIDENCE_KEY_FILE is not an absolute path: the extractor stays off\n');
68
+ return null;
69
+ }
70
+ try {
71
+ if (!node_fs_1.default.statSync(keyFile).isFile())
72
+ throw new Error('not a regular file');
73
+ const key = node_fs_1.default.readFileSync(keyFile, 'utf8').trim();
74
+ if (key === '' || /\s/.test(key))
75
+ throw new Error('empty or not a single token');
76
+ return key;
77
+ }
78
+ catch (err) {
79
+ process.stderr.write(`[evidence] opt-in is on but the key file ${keyFile} cannot be used (${err instanceof Error ? err.message.replace(keyFile, 'it') : 'unreadable'}): the extractor stays off\n`);
80
+ return null;
81
+ }
82
+ }
83
+ /** Opt-in sends the Ollama key, so it only ever goes to Ollama Cloud. */
84
+ function optinEndpointAllowed(baseUrl) {
85
+ if ((0, keyPool_1.isOllamaCloudEndpoint)(baseUrl))
86
+ return true;
87
+ process.stderr.write('[evidence] opt-in is on but RENOR_EVIDENCE_BASE_URL is not Ollama Cloud: the extractor stays off\n');
88
+ return false;
89
+ }
90
+ /** The workspace used its allowance for today (or its allowance could not be read, which is treated the same). */
91
+ class EvidenceCapError extends Error {
92
+ constructor(message = 'the daily allowance of evidence-model calls is used') {
93
+ super(message);
94
+ this.name = 'EvidenceCapError';
95
+ }
96
+ }
97
+ exports.EvidenceCapError = EvidenceCapError;
98
+ const DAY_MS = 24 * 60 * 60 * 1000;
99
+ const KEEP_DAYS = 14;
100
+ function utcDay(now) {
101
+ return new Date(now).toISOString().slice(0, 10);
102
+ }
103
+ /**
104
+ * Model calls counted per UTC day in the workspace's own evidence-cache file, so a restart does not hand back
105
+ * the day's allowance. `take` reserves one call before each HTTP attempt and is atomic across processes (one write
106
+ * transaction), so a concurrent search can never overshoot by more than a call already in flight and refused.
107
+ */
108
+ class EvidenceDailyBudget {
109
+ limit;
110
+ now;
111
+ db;
112
+ takeStmt;
113
+ usedStmt;
114
+ sweepStmt;
115
+ constructor(dbPath, limit, now = Date.now) {
116
+ this.limit = limit;
117
+ this.now = now;
118
+ this.db = (0, client_1.openDb)(dbPath);
119
+ this.db.pragma('journal_mode = WAL');
120
+ this.db.pragma('busy_timeout = 10000');
121
+ this.db.exec('CREATE TABLE IF NOT EXISTS evidence_daily_calls (day TEXT PRIMARY KEY, calls INTEGER NOT NULL)');
122
+ this.takeStmt = this.db.prepare(`INSERT INTO evidence_daily_calls (day, calls) VALUES (?, 1)
123
+ ON CONFLICT(day) DO UPDATE SET calls = calls + 1 WHERE calls < ?`);
124
+ this.usedStmt = this.db.prepare('SELECT calls FROM evidence_daily_calls WHERE day = ?');
125
+ this.sweepStmt = this.db.prepare('DELETE FROM evidence_daily_calls WHERE day < ?');
126
+ }
127
+ /** Reserve one call for today; false when the day's allowance is spent. */
128
+ take() {
129
+ const now = this.now();
130
+ const day = utcDay(now);
131
+ if (this.limit <= 0)
132
+ return false;
133
+ return this.db.transaction(() => {
134
+ this.sweepStmt.run(utcDay(now - KEEP_DAYS * DAY_MS));
135
+ return this.takeStmt.run(day, this.limit).changes > 0;
136
+ }).immediate();
137
+ }
138
+ used() {
139
+ return this.usedStmt.get(utcDay(this.now()))?.calls ?? 0;
140
+ }
141
+ }
142
+ exports.EvidenceDailyBudget = EvidenceDailyBudget;
143
+ /**
144
+ * A `beforeAttempt` for createOllamaChat that reserves one call per HTTP attempt, retries included, and throws
145
+ * EvidenceCapError when none is left. A budget that cannot be opened refuses every attempt: a shared provider
146
+ * limit is not worth risking on a counter that is not there.
147
+ */
148
+ function dailyBudgetGate(open) {
149
+ let budget;
150
+ return () => {
151
+ if (budget === undefined) {
152
+ try {
153
+ budget = open();
154
+ }
155
+ catch (err) {
156
+ process.stderr.write(`[evidence] the daily call counter cannot be opened (${err instanceof Error ? err.message : String(err)}): the extractor stays off\n`);
157
+ budget = null;
158
+ }
159
+ }
160
+ if (!budget)
161
+ throw new EvidenceCapError('the daily call counter is unavailable');
162
+ if (!budget.take())
163
+ throw new EvidenceCapError();
164
+ };
165
+ }
@@ -0,0 +1,130 @@
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.resolveEvidenceBounds = resolveEvidenceBounds;
7
+ exports.resolveEvidenceProvider = resolveEvidenceProvider;
8
+ exports.resolveEvidenceToolOptions = resolveEvidenceToolOptions;
9
+ exports.createEvidenceExtractor = createEvidenceExtractor;
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const ollamaChat_1 = require("./ollamaChat");
12
+ const keyPool_1 = require("./keyPool");
13
+ const evidenceCache_1 = require("./evidenceCache");
14
+ const optin_1 = require("./optin");
15
+ const gate_1 = require("./gate");
16
+ const secondPass_1 = require("./secondPass");
17
+ /**
18
+ * RENOR_EVIDENCE_MAX_TOKENS (a positive integer) and
19
+ * RENOR_EVIDENCE_REASONING_EFFORT (none | minimal | low | medium | high).
20
+ * Unset or blank means no bound, which is the shipped request. A value that
21
+ * does not parse throws, like an invalid RENOR_EVIDENCE_MODE: an operator who
22
+ * set a bound must not get an unbounded extractor because of a typo.
23
+ */
24
+ function resolveEvidenceBounds(env = process.env) {
25
+ const bounds = {};
26
+ const maxTokens = (env.RENOR_EVIDENCE_MAX_TOKENS ?? '').trim();
27
+ if (maxTokens !== '') {
28
+ const value = Number(maxTokens);
29
+ if (!/^[1-9][0-9]*$/.test(maxTokens) || !Number.isSafeInteger(value)) {
30
+ throw new Error(`Invalid RENOR_EVIDENCE_MAX_TOKENS "${maxTokens}": must be a positive integer`);
31
+ }
32
+ bounds.maxTokens = value;
33
+ }
34
+ const effort = (env.RENOR_EVIDENCE_REASONING_EFFORT ?? '').trim().toLowerCase();
35
+ if (effort !== '') {
36
+ if (!secondPass_1.EVIDENCE_REASONING_EFFORTS.includes(effort)) {
37
+ throw new Error(`Invalid RENOR_EVIDENCE_REASONING_EFFORT "${effort}": must be ${secondPass_1.EVIDENCE_REASONING_EFFORTS.join(' | ')}`);
38
+ }
39
+ bounds.reasoningEffort = effort;
40
+ }
41
+ return bounds;
42
+ }
43
+ /**
44
+ * The OpenAI-compatible provider the query-time model passes call: RENOR_EVIDENCE_BASE_URL (unset or blank: the
45
+ * transport's Ollama Cloud default) and the key pool. Shared by the evidence pass and the session router
46
+ * (src/router/sessionRouter.ts), so the two can never disagree about which keys exist, and the URL returned is the
47
+ * one the Ollama keys were judged against. Values are never logged.
48
+ */
49
+ function resolveEvidenceProvider(env = process.env) {
50
+ // Blank is the default endpoint (Ollama Cloud), like an unset variable.
51
+ const baseUrl = env.RENOR_EVIDENCE_BASE_URL?.trim() || undefined;
52
+ const keys = [
53
+ ...(env.RENOR_EVIDENCE_API_KEYS ?? '').split(','),
54
+ env.RENOR_EVIDENCE_API_KEY ?? '',
55
+ // The Ollama pool is collected by the shared helper, never re-listed here:
56
+ // this module used to name `OLLAMA_CLOUD_KEY_1..3` while the write-path
57
+ // extractor named six, so with the first three keys at their monthly
58
+ // ceiling the evidence layer vanished for 77 of 500 cases with no signal in
59
+ // the manifest (issue #222). One list, two readers. And only for an Ollama
60
+ // Cloud endpoint: another provider never receives an Ollama key.
61
+ ...(0, keyPool_1.ollamaKeysForEndpoint)(baseUrl, env).keys,
62
+ ].map(k => k.trim()).filter(Boolean);
63
+ return { baseUrl, keys };
64
+ }
65
+ function resolveEvidenceToolOptions(config, env = process.env) {
66
+ const mode = (0, gate_1.resolveEvidenceMode)(env);
67
+ const model = env.RENOR_EVIDENCE_MODEL?.trim() || secondPass_1.EVIDENCE_MODEL_DEFAULT;
68
+ const cachePath = env.RENOR_EVIDENCE_CACHE?.trim()
69
+ || node_path_1.default.join(node_path_1.default.dirname(config.dbPath), 'evidence-cache.db');
70
+ const bounds = resolveEvidenceBounds(env);
71
+ // A workspace that opted in (RENOR_EVIDENCE_OPTIN, see optin.ts) reaches Ollama Cloud with the key file's key
72
+ // alone, on gemma4:31b, and spends a daily allowance of calls. Unset, this is the resolver it always was.
73
+ const optin = (0, optin_1.resolveEvidenceOptin)(env);
74
+ const { baseUrl, keys } = resolveEvidenceProvider(env);
75
+ // Coverage knob (RENOR_EVIDENCE_SESSIONS). Default 20 matches the
76
+ // promotion-grade full-500 arm. One model call per uncached search,
77
+ // regardless of session count; explicit values remain bounded.
78
+ // A value that floors below 1 is treated as UNSET, not as "0 sessions". A
79
+ // fractional setting is finite and positive, so a bare `> 0` test let it
80
+ // through and `Math.floor` turned it into 0 — and 0 is not a silent no-op:
81
+ // `loadEvidenceSessionsFromVault` rejects it (`maxSessions must be > 0`),
82
+ // which the `evidence` MCP tool surfaces as a thrown error and the search path
83
+ // swallows as `extractor-error`. Floor first, then require >= 1.
84
+ const requested = Number(env.RENOR_EVIDENCE_SESSIONS ?? '');
85
+ const requestedSessions = Number.isFinite(requested) ? Math.floor(requested) : Number.NaN;
86
+ const maxSessions = Number.isFinite(requestedSessions) && requestedSessions >= 1
87
+ ? Math.min(requestedSessions, 50)
88
+ : secondPass_1.MAX_EVIDENCE_SESSIONS;
89
+ let chat;
90
+ if (optin) {
91
+ const optinKey = (0, optin_1.optinEndpointAllowed)(baseUrl) ? (0, optin_1.readOptinKey)(optin.keyFile) : null;
92
+ if (optinKey) {
93
+ const dailyCap = optin.dailyCap;
94
+ // The allowance is spent per HTTP attempt (a retried 429 or 5xx is another request to the provider), so it
95
+ // sits inside the transport rather than around it.
96
+ chat = (0, ollamaChat_1.createOllamaChat)({
97
+ keys: [optinKey],
98
+ beforeAttempt: (0, optin_1.dailyBudgetGate)(() => new optin_1.EvidenceDailyBudget(cachePath, dailyCap)),
99
+ });
100
+ }
101
+ }
102
+ else if (keys.length > 0) {
103
+ chat = (0, ollamaChat_1.createOllamaChat)({ baseUrl, keys });
104
+ }
105
+ return {
106
+ mode,
107
+ // The opt-in is the gemma4:31b decision: a stray RENOR_EVIDENCE_MODEL does not change what the privacy page names.
108
+ model: optin ? secondPass_1.EVIDENCE_MODEL_DEFAULT : model,
109
+ maxSessions,
110
+ charCap: secondPass_1.EVIDENCE_SESSION_CHAR_CAP,
111
+ cachePath,
112
+ ...(chat ? { chat } : {}),
113
+ ...(optin ? { dailyCap: optin.dailyCap } : {}),
114
+ ...(bounds.maxTokens !== undefined || bounds.reasoningEffort !== undefined ? { bounds } : {}),
115
+ };
116
+ }
117
+ function createEvidenceExtractor(evidence) {
118
+ if (!evidence.chat)
119
+ return null;
120
+ let cache = null;
121
+ if (evidence.cachePath) {
122
+ try {
123
+ cache = new evidenceCache_1.EvidenceCache(evidence.cachePath);
124
+ }
125
+ catch (err) {
126
+ process.stderr.write(`[evidence] cache unavailable: ${err instanceof Error ? err.message : String(err)}\n`);
127
+ }
128
+ }
129
+ return new secondPass_1.EvidenceExtractor({ chat: evidence.chat, cache, model: evidence.model, bounds: evidence.bounds });
130
+ }