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,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SERVER_ENDPOINT = exports.SERVER_TITLE = exports.SERVER_VERSION = exports.SERVER_NAME = void 0;
4
+ exports.buildServerCard = buildServerCard;
5
+ exports.serverCardText = serverCardText;
6
+ /** The MCP server's identity: its initialize reply and this card both take it from here. */
7
+ exports.SERVER_NAME = 'renor';
8
+ exports.SERVER_VERSION = '0.1.0';
9
+ exports.SERVER_TITLE = 'Renor';
10
+ /** The hosted server's one public endpoint (the gateway's /mcp), Streamable HTTP. */
11
+ exports.SERVER_ENDPOINT = 'https://mcp.renor.cloud/mcp';
12
+ function byName(a, b) {
13
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
14
+ }
15
+ /** The card for the tools an agent sees, in name order, so the file does not depend on registration order. */
16
+ function buildServerCard(tools) {
17
+ return {
18
+ serverInfo: { name: exports.SERVER_NAME, title: exports.SERVER_TITLE, version: exports.SERVER_VERSION },
19
+ remotes: [{ type: 'streamable-http', url: exports.SERVER_ENDPOINT }],
20
+ authentication: { required: true, schemes: ['oauth2'] },
21
+ tools: [...tools].sort(byName),
22
+ resources: [],
23
+ prompts: [],
24
+ };
25
+ }
26
+ /** The exact bytes the gateway serves: two-space JSON with a final newline. */
27
+ function serverCardText(card) {
28
+ return `${JSON.stringify(card, null, 2)}\n`;
29
+ }
@@ -0,0 +1,139 @@
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.StoredSizeGauge = void 0;
7
+ exports.measureMarkdownBytes = measureMarkdownBytes;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ /**
11
+ * How many bytes of Markdown a workspace stores: its notes and the engine's own ledger, every regular `.md` file under
12
+ * the memory directory. The search index (`.memory-index/`), the caches and the auth state are derived or private
13
+ * state, not the user's memory, and are not counted. A symbolic link is never followed.
14
+ *
15
+ * The figure goes to the gateway on the `/internal/gateway-token` call it makes for every request, so the control plane
16
+ * can record it, show it, and cap writes, without a call of its own and without waking an engine to ask: an engine that
17
+ * is asleep has not changed what it stores.
18
+ */
19
+ /** The folder holding the index. It sits inside the memory directory and is never user content. */
20
+ const INDEX_DIR = '.memory-index';
21
+ /** Files stat'ed at once: enough to hide the latency, few enough to stay polite to a small VM. */
22
+ const STAT_CHUNK = 64;
23
+ /** The bytes of every regular `.md` file below `root`. Unreadable content fails measurement; vanished files are skipped. */
24
+ async function measureMarkdownBytes(root) {
25
+ let total = 0;
26
+ const folders = [root];
27
+ while (folders.length > 0) {
28
+ const folder = folders.pop();
29
+ let entries;
30
+ try {
31
+ entries = await fs_1.default.promises.readdir(folder, { withFileTypes: true });
32
+ }
33
+ catch (err) {
34
+ // A folder that vanished while the walk was running is not an error; an unreadable root is.
35
+ if (folder === root || err.code !== 'ENOENT')
36
+ throw err;
37
+ continue;
38
+ }
39
+ const files = [];
40
+ for (const entry of entries) {
41
+ // `isDirectory()` and `isFile()` are false for a symbolic link, so a link is skipped, never followed.
42
+ if (entry.isDirectory()) {
43
+ if (entry.name !== INDEX_DIR)
44
+ folders.push(path_1.default.join(folder, entry.name));
45
+ }
46
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
47
+ files.push(path_1.default.join(folder, entry.name));
48
+ }
49
+ }
50
+ for (let i = 0; i < files.length; i += STAT_CHUNK) {
51
+ const sizes = await Promise.all(files.slice(i, i + STAT_CHUNK).map(async (file) => {
52
+ try {
53
+ const stat = await fs_1.default.promises.lstat(file);
54
+ return stat.isFile() ? stat.size : 0;
55
+ }
56
+ catch (err) {
57
+ if (err.code !== 'ENOENT')
58
+ throw err;
59
+ return 0; // deleted between the listing and the stat
60
+ }
61
+ }));
62
+ for (const size of sizes)
63
+ total += size;
64
+ }
65
+ }
66
+ return total;
67
+ }
68
+ /**
69
+ * The last measured size, served at once by read(). It never blocks and never throws: a missing or stale figure only starts one
70
+ * measurement in the background (one at a time), whose result the next caller gets. The first measurement starts when
71
+ * the gauge is made, so early reporting calls can have a figure. A measurement that fails keeps the last good
72
+ * figure and is retried after the TTL, not on every reporting call. readFresh() instead awaits a current walk for admission.
73
+ */
74
+ class StoredSizeGauge {
75
+ dir;
76
+ bytes = null;
77
+ measuredAt = 0;
78
+ running = null;
79
+ ttlMs;
80
+ now;
81
+ measure;
82
+ constructor(dir, options = {}) {
83
+ this.dir = dir;
84
+ this.ttlMs = options.ttlMs ?? 30_000;
85
+ this.now = options.now ?? Date.now;
86
+ this.measure = options.measure ?? measureMarkdownBytes;
87
+ this.refresh();
88
+ }
89
+ /** The bytes as of the last finished measurement, or null before the first one has finished. */
90
+ read() {
91
+ if (this.bytes === null || this.now() - this.measuredAt >= this.ttlMs)
92
+ this.refresh();
93
+ return this.bytes;
94
+ }
95
+ refresh() {
96
+ if (this.running)
97
+ return;
98
+ // Started now, and whatever a measurement does, even a synchronous throw, stays out of the caller's way.
99
+ let attempt;
100
+ try {
101
+ attempt = Promise.resolve(this.measure(this.dir));
102
+ }
103
+ catch (err) {
104
+ attempt = Promise.reject(err);
105
+ }
106
+ this.running = attempt
107
+ .then((bytes) => {
108
+ if (Number.isSafeInteger(bytes) && bytes >= 0)
109
+ this.bytes = bytes;
110
+ }, () => {
111
+ // Keep the last good figure.
112
+ })
113
+ .finally(() => {
114
+ this.measuredAt = this.now();
115
+ this.running = null;
116
+ });
117
+ }
118
+ /** Admission checks must measure after previous writes, never reuse the TTL or an in-flight older walk. */
119
+ async readFresh() {
120
+ await this.settled();
121
+ try {
122
+ const bytes = await this.measure(this.dir);
123
+ if (!Number.isSafeInteger(bytes) || bytes < 0)
124
+ return null;
125
+ this.bytes = bytes;
126
+ this.measuredAt = this.now();
127
+ return bytes;
128
+ }
129
+ catch {
130
+ // A cached good value cannot authorize a write after a failed current measurement.
131
+ return null;
132
+ }
133
+ }
134
+ /** Resolves when the measurement in flight, if any, has finished. */
135
+ settled() {
136
+ return this.running ?? Promise.resolve();
137
+ }
138
+ }
139
+ exports.StoredSizeGauge = StoredSizeGauge;
@@ -0,0 +1,441 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RECALL_TOOLS = exports.REJECTION_KINDS = void 0;
4
+ exports.isRecallTool = isRecallTool;
5
+ exports.isPlatformInitialize = isPlatformInitialize;
6
+ exports.isSafeArgName = isSafeArgName;
7
+ exports.telemetryInstanceId = telemetryInstanceId;
8
+ exports.emitTelemetry = emitTelemetry;
9
+ exports.flushTelemetry = flushTelemetry;
10
+ const node_crypto_1 = require("node:crypto");
11
+ const toolRefusal_1 = require("../toolRefusal");
12
+ /**
13
+ * How a tool's input schema refused a call: a CLOSED set, so a kind can name the family of the failure and nothing the
14
+ * caller wrote. `missing`: the argument (or all of them) was absent. `wrong_type`: present, but not the type the
15
+ * schema takes (a number sent as a string, a null). `too_small` / `too_big`: under a minimum or over a maximum, of a length
16
+ * or of a value. `invalid_value`: not one of the values the schema lists. `invalid_format`: not the format it takes
17
+ * (a UUID, a pattern). `other`: any refusal this list has no word for.
18
+ */
19
+ exports.REJECTION_KINDS = ['missing', 'wrong_type', 'too_small', 'too_big', 'invalid_value', 'invalid_format', 'other'];
20
+ const SAFE_TOOL = /^[a-z][a-z0-9_]{0,64}$/;
21
+ const SAFE_ARG = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
22
+ const OPAQUE_ID = /^[0-9a-f]{8}$/;
23
+ const SESSION_ID = /^[0-9a-f]{16}$/;
24
+ /** Ids a `read` event lists, as many as readUsefulness.ts lists: `items` still counts them all. */
25
+ const MAX_EVENT_IDS = 20;
26
+ // The values a field that is not free text may take. The type of each field is made from its list, so the two cannot part.
27
+ const TOOL_ERROR_KINDS = ['validation', 'unavailable', 'internal', 'auth', 'other'];
28
+ const AUTH_KINDS = ['login_ok', 'login_fail', 'token_ok', 'token_fail'];
29
+ const SESSION_KINDS = ['mcp_init', 'mcp_close'];
30
+ const READ_TOOLS = ['get_context', 'search_memory', 'retrieve_memory'];
31
+ const USE_VIAS = ['read', 'cite', 'update'];
32
+ /**
33
+ * The tools that read stored memory back to the agent: the usage report's "memory read" tools (docs/USAGE-METRICS.md) and
34
+ * read_memory. A first call to one of them is a recall. Decided here, from the tool's name, so no caller can set the flag.
35
+ */
36
+ exports.RECALL_TOOLS = [
37
+ 'get_context', 'search_memory', 'search_text', 'search_history', 'retrieve_memory', 'lookup_entity', 'export_memory', 'read_memory',
38
+ ];
39
+ function isRecallTool(tool) {
40
+ return exports.RECALL_TOOLS.includes(tool);
41
+ }
42
+ /**
43
+ * The `clientInfo.name` the platform's own MCP calls present: the gateway's dashboard import (engine/apps/gateway/src/dashboard-import.ts)
44
+ * and the dashboard's memory inventory (supabase/functions/memory-inventory/renor-tenant.ts). A session that presents one is the
45
+ * platform acting for a person, not an agent reaching for memory, so the recall share leaves it out. The name is what the client
46
+ * declares about itself: a client that declares one only drops its own session from the share, and nothing else follows from it.
47
+ */
48
+ const PLATFORM_CLIENT_NAMES = new Set(['renor-dashboard', 'renor-dashboard-import']);
49
+ /** True when an `initialize` request names a platform client (see PLATFORM_CLIENT_NAMES). Reads the body; keeps nothing from it. */
50
+ function isPlatformInitialize(body) {
51
+ const params = typeof body === 'object' && body !== null ? body.params : undefined;
52
+ const clientInfo = typeof params === 'object' && params !== null ? params.clientInfo : undefined;
53
+ const name = typeof clientInfo === 'object' && clientInfo !== null ? clientInfo.name : undefined;
54
+ return typeof name === 'string' && PLATFORM_CLIENT_NAMES.has(name);
55
+ }
56
+ const READ_FIELDS = ['event', 'tool', 'read', 'relevant', 'judged', 'items', 'ids'];
57
+ const READ_USED_FIELDS = ['event', 'tool', 'read', 'via', 'after_s'];
58
+ const FIRST_CALL_FIELDS = ['event', 'tool', 'recall', 'session'];
59
+ /** Exactly these keys and no other: a field nobody listed is a field nobody reviewed, so the event is dropped whole. */
60
+ function hasOnlyFields(payload, allowed) {
61
+ const keys = Object.keys(payload);
62
+ return keys.length === allowed.length && keys.every(key => allowed.includes(key));
63
+ }
64
+ const isOpaqueId = (value) => typeof value === 'string' && OPAQUE_ID.test(value);
65
+ const isSessionId = (value) => typeof value === 'string' && SESSION_ID.test(value);
66
+ const isCount = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0;
67
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
68
+ const isSafeTool = (value) => typeof value === 'string' && SAFE_TOOL.test(value);
69
+ /** An argument name as the tools declare them: an identifier. Anything else is left unnamed, and the call is still counted. */
70
+ function isSafeArgName(name) {
71
+ return typeof name === 'string' && SAFE_ARG.test(name);
72
+ }
73
+ /** The member of `allowed` that `value` is, or undefined. Only a string can be one, and what comes back is the list's own string. */
74
+ function oneOf(value, allowed) {
75
+ return typeof value === 'string' ? allowed.find(member => member === value) : undefined;
76
+ }
77
+ /**
78
+ * `ids` as a NEW array of opaque ids, or null. It walks the indexes itself and never asks the array anything: an array can carry
79
+ * its own `every` or `toJSON`, and `every` skips a hole that JSON.stringify writes as `null`. Each element is read once and what
80
+ * is kept is the element as it was checked.
81
+ */
82
+ function opaqueIds(value) {
83
+ if (!Array.isArray(value))
84
+ return null;
85
+ const length = value.length;
86
+ if (typeof length !== 'number' || !(length <= MAX_EVENT_IDS))
87
+ return null;
88
+ const ids = [];
89
+ for (let index = 0; index < length; index++) {
90
+ if (!Object.prototype.hasOwnProperty.call(value, index))
91
+ return null;
92
+ const id = value[index];
93
+ if (!isOpaqueId(id))
94
+ return null;
95
+ ids.push(id);
96
+ }
97
+ return ids;
98
+ }
99
+ /**
100
+ * The one door out: what is written is what comes out of here. The payload belongs to the caller, so it is only read: each field
101
+ * once, into a local, and checked by its TYPE (a regex or an `includes` on an object coerces it through its own toString and
102
+ * passes, while JSON.stringify then writes the object out as it is). The event that leaves is a NEW object made of those checked
103
+ * values, so nothing the caller still owns can reach a sink: not a field nobody listed, not an accessor that answers differently
104
+ * the second time, not an array's own `every` or `toJSON`. A value that fails its check drops the whole event; a read event
105
+ * is dropped for a field nobody listed too, where the older events just leave that field out.
106
+ */
107
+ function cleanEvent(payload) {
108
+ if (typeof payload !== 'object' || payload === null)
109
+ return null;
110
+ const fields = payload;
111
+ switch (fields.event) {
112
+ case 'tool_use': {
113
+ const { tool, ok, ms, error_kind, error_reason } = fields;
114
+ if (!isSafeTool(tool) || typeof ok !== 'boolean' || !isFiniteNumber(ms))
115
+ return null;
116
+ const errorKind = error_kind === undefined ? undefined : oneOf(error_kind, TOOL_ERROR_KINDS);
117
+ if (error_kind !== undefined && !errorKind)
118
+ return null;
119
+ // The closed set is the whole gate for a reason: anything that is not one of its literals is dropped
120
+ // rather than logged, so no code can be a message, a statement or a path however it was produced. A
121
+ // dropped reason leaves the call counted, exactly as it was before reasons existed.
122
+ const errorReason = (0, toolRefusal_1.isToolErrorReason)(error_reason) ? error_reason : undefined;
123
+ return {
124
+ event: 'tool_use',
125
+ tool,
126
+ ok,
127
+ ms,
128
+ ...(errorKind ? { error_kind: errorKind } : {}),
129
+ ...(errorReason ? { error_reason: errorReason } : {}),
130
+ };
131
+ }
132
+ case 'auth': {
133
+ const kind = oneOf(fields.kind, AUTH_KINDS);
134
+ return kind ? { event: 'auth', kind } : null;
135
+ }
136
+ case 'session': {
137
+ const { kind: rawKind, session } = fields;
138
+ const kind = oneOf(rawKind, SESSION_KINDS);
139
+ if (!kind)
140
+ return null;
141
+ if (session === undefined)
142
+ return { event: 'session', kind };
143
+ return isSessionId(session) ? { event: 'session', kind, session } : null;
144
+ }
145
+ // A session's first tool call: the tool, whether it is a recall and the opaque id of the session. Nothing else: not the
146
+ // call's arguments, its result or its timing. `recall` must be the tool's own answer from RECALL_TOOLS, or the event is dropped.
147
+ case 'first_call': {
148
+ if (!hasOnlyFields(fields, FIRST_CALL_FIELDS))
149
+ return null;
150
+ const { tool, recall, session } = fields;
151
+ if (!isSafeTool(tool) || typeof recall !== 'boolean' || recall !== isRecallTool(tool) || !isSessionId(session))
152
+ return null;
153
+ return { event: 'first_call', tool, recall, session };
154
+ }
155
+ // A rejection line may name a tool and an argument and say how it failed, from the closed set: nothing else gets out.
156
+ case 'tool_rejected': {
157
+ const { tool, arg, kind } = fields;
158
+ const rejection = oneOf(kind, exports.REJECTION_KINDS);
159
+ if (!isSafeTool(tool) || !rejection)
160
+ return null;
161
+ if (arg === undefined)
162
+ return { event: 'tool_rejected', tool, kind: rejection };
163
+ return isSafeArgName(arg) ? { event: 'tool_rejected', tool, arg, kind: rejection } : null;
164
+ }
165
+ // The read-usefulness events carry nothing but counts and opaque ids.
166
+ case 'read': {
167
+ if (!hasOnlyFields(fields, READ_FIELDS))
168
+ return null;
169
+ const { tool, read, relevant, judged, items, ids } = fields;
170
+ const readTool = oneOf(tool, READ_TOOLS);
171
+ const opaque = opaqueIds(ids);
172
+ if (!readTool || !isOpaqueId(read) || typeof relevant !== 'boolean' || typeof judged !== 'boolean' || !isCount(items) || !opaque)
173
+ return null;
174
+ return { event: 'read', tool: readTool, read, relevant, judged, items, ids: opaque };
175
+ }
176
+ case 'read_used': {
177
+ if (!hasOnlyFields(fields, READ_USED_FIELDS))
178
+ return null;
179
+ const { tool, read, via, after_s } = fields;
180
+ const readTool = oneOf(tool, READ_TOOLS);
181
+ const usedVia = oneOf(via, USE_VIAS);
182
+ if (!readTool || !isOpaqueId(read) || !usedVia || !isCount(after_s))
183
+ return null;
184
+ return { event: 'read_used', tool: readTool, read, via: usedVia, after_s };
185
+ }
186
+ default:
187
+ return null;
188
+ }
189
+ }
190
+ const pendingRemote = new Set();
191
+ function env(name) {
192
+ return (process.env[name] ?? '').trim();
193
+ }
194
+ function remoteDisabled() {
195
+ return env('RENOR_TELEMETRY_DISABLED').toLowerCase() === 'true';
196
+ }
197
+ function pseudonymousInstance(instance) {
198
+ const salt = env('RENOR_TELEMETRY_HASH_SALT');
199
+ return (0, node_crypto_1.createHash)('sha256')
200
+ .update(`renor-telemetry:\0${salt}\0${instance}`)
201
+ .digest('hex')
202
+ .slice(0, 24);
203
+ }
204
+ function isFailure(line) {
205
+ if (line.event === 'tool_use')
206
+ return !line.ok;
207
+ if (line.event === 'auth')
208
+ return line.kind.endsWith('_fail');
209
+ return false;
210
+ }
211
+ function safeTimeoutMs() {
212
+ const parsed = Number(env('RENOR_TELEMETRY_TIMEOUT_MS') || '1500');
213
+ if (!Number.isFinite(parsed))
214
+ return 1500;
215
+ return Math.min(5000, Math.max(250, Math.trunc(parsed)));
216
+ }
217
+ async function postJson(url, init) {
218
+ try {
219
+ const response = await fetch(url, {
220
+ ...init,
221
+ signal: AbortSignal.timeout(safeTimeoutMs()),
222
+ });
223
+ await response.body?.cancel();
224
+ }
225
+ catch {
226
+ // Remote observability must never affect Renor availability.
227
+ }
228
+ }
229
+ function posthogEndpoint() {
230
+ const explicit = env('RENOR_POSTHOG_CAPTURE_URL');
231
+ if (explicit)
232
+ return explicit;
233
+ const host = env('RENOR_POSTHOG_HOST') || 'https://us.i.posthog.com';
234
+ return `${host.replace(/\/$/, '')}/i/v0/e/`;
235
+ }
236
+ async function sendPostHog(line) {
237
+ const apiKey = env('RENOR_POSTHOG_KEY');
238
+ if (!apiKey)
239
+ return;
240
+ const { event, instance: _instance, ts, ...properties } = line;
241
+ await postJson(posthogEndpoint(), {
242
+ method: 'POST',
243
+ headers: { 'content-type': 'application/json' },
244
+ body: JSON.stringify({
245
+ api_key: apiKey,
246
+ distinct_id: pseudonymousInstance(line.instance),
247
+ event: `renor_${event}`,
248
+ timestamp: ts,
249
+ properties: {
250
+ service: 'renor-core',
251
+ $process_person_profile: false,
252
+ ...properties,
253
+ },
254
+ }),
255
+ });
256
+ }
257
+ function sentryEnvelopeEndpoint(dsn) {
258
+ try {
259
+ const parsed = new URL(dsn);
260
+ const parts = parsed.pathname.split('/').filter(Boolean);
261
+ const projectId = parts.pop();
262
+ if (!projectId || !/^\d+$/.test(projectId))
263
+ return null;
264
+ const prefix = parts.length ? `/${parts.join('/')}` : '';
265
+ return `${parsed.origin}${prefix}/api/${projectId}/envelope/`;
266
+ }
267
+ catch {
268
+ return null;
269
+ }
270
+ }
271
+ async function sendSentry(line) {
272
+ if (!isFailure(line))
273
+ return;
274
+ const dsn = env('RENOR_SENTRY_DSN');
275
+ if (!dsn)
276
+ return;
277
+ const endpoint = sentryEnvelopeEndpoint(dsn);
278
+ if (!endpoint)
279
+ return;
280
+ const eventId = (0, node_crypto_1.randomBytes)(16).toString('hex');
281
+ const tags = {
282
+ service: 'renor-core',
283
+ event: line.event,
284
+ instance_id: pseudonymousInstance(line.instance),
285
+ };
286
+ const extra = {};
287
+ if (line.event === 'tool_use') {
288
+ tags.tool = line.tool;
289
+ tags.error_kind = line.error_kind ?? 'other';
290
+ extra.ok = line.ok;
291
+ extra.ms = line.ms;
292
+ if (line.error_reason)
293
+ tags.error_reason = line.error_reason;
294
+ }
295
+ else if (line.event === 'auth') {
296
+ tags.kind = line.kind;
297
+ }
298
+ const payload = {
299
+ event_id: eventId,
300
+ timestamp: line.ts,
301
+ level: 'error',
302
+ platform: 'node',
303
+ message: 'Renor operational failure',
304
+ tags,
305
+ extra,
306
+ };
307
+ const envelope = JSON.stringify({ event_id: eventId, sent_at: line.ts, dsn }) +
308
+ '\n' +
309
+ JSON.stringify({ type: 'event' }) +
310
+ '\n' +
311
+ JSON.stringify(payload);
312
+ await postJson(endpoint, {
313
+ method: 'POST',
314
+ headers: { 'content-type': 'application/x-sentry-envelope' },
315
+ body: envelope,
316
+ });
317
+ }
318
+ function otlpAttributes(line) {
319
+ const attrs = [
320
+ { key: 'renor.event', value: { stringValue: line.event } },
321
+ {
322
+ key: 'renor.instance_id',
323
+ value: { stringValue: pseudonymousInstance(line.instance) },
324
+ },
325
+ ];
326
+ if (line.event === 'tool_use') {
327
+ attrs.push({ key: 'renor.tool', value: { stringValue: line.tool } }, { key: 'renor.ok', value: { boolValue: line.ok } }, { key: 'renor.duration_ms', value: { intValue: String(Math.max(0, Math.trunc(line.ms))) } });
328
+ if (line.error_kind) {
329
+ attrs.push({ key: 'renor.error_kind', value: { stringValue: line.error_kind } });
330
+ }
331
+ if (line.error_reason) {
332
+ attrs.push({ key: 'renor.error_reason', value: { stringValue: line.error_reason } });
333
+ }
334
+ }
335
+ else if (line.event === 'auth') {
336
+ attrs.push({ key: 'renor.auth_kind', value: { stringValue: line.kind } });
337
+ }
338
+ else if (line.event === 'tool_rejected') {
339
+ attrs.push({ key: 'renor.tool', value: { stringValue: line.tool } }, { key: 'renor.rejection_kind', value: { stringValue: line.kind } });
340
+ if (line.arg)
341
+ attrs.push({ key: 'renor.rejected_arg', value: { stringValue: line.arg } });
342
+ }
343
+ else if (line.event === 'session') {
344
+ attrs.push({ key: 'renor.session_kind', value: { stringValue: line.kind } });
345
+ }
346
+ return attrs;
347
+ }
348
+ async function sendLangfuse(line) {
349
+ // A trace span per call; a read-usefulness event is a count, and PostHog takes it. A first call is already a tool_use span.
350
+ if (line.event === 'read' || line.event === 'read_used' || line.event === 'first_call')
351
+ return;
352
+ const publicKey = env('LANGFUSE_PUBLIC_KEY');
353
+ const secretKey = env('LANGFUSE_SECRET_KEY');
354
+ if (!publicKey || !secretKey)
355
+ return;
356
+ const host = (env('LANGFUSE_HOST') || 'https://cloud.langfuse.com').replace(/\/$/, '');
357
+ const endMs = Date.parse(line.ts);
358
+ if (!Number.isFinite(endMs))
359
+ return;
360
+ const durationMs = line.event === 'tool_use' ? Math.max(0, line.ms) : 0;
361
+ const startMs = Math.max(0, endMs - durationMs);
362
+ const traceId = (0, node_crypto_1.randomBytes)(16).toString('hex');
363
+ const spanId = (0, node_crypto_1.randomBytes)(8).toString('hex');
364
+ const suffix = line.event === 'tool_use' || line.event === 'tool_rejected' ? line.tool :
365
+ line.event === 'auth' || line.event === 'session' ? line.kind :
366
+ 'read';
367
+ const body = {
368
+ resourceSpans: [
369
+ {
370
+ resource: {
371
+ attributes: [
372
+ { key: 'service.name', value: { stringValue: 'renor-core' } },
373
+ ],
374
+ },
375
+ scopeSpans: [
376
+ {
377
+ scope: { name: 'renor.telemetry', version: '1' },
378
+ spans: [
379
+ {
380
+ traceId,
381
+ spanId,
382
+ name: `renor.${line.event}.${suffix}`,
383
+ startTimeUnixNano: String(BigInt(Math.trunc(startMs)) * 1000000n),
384
+ endTimeUnixNano: String(BigInt(Math.trunc(endMs)) * 1000000n),
385
+ attributes: otlpAttributes(line),
386
+ status: { code: isFailure(line) ? 2 : 1 },
387
+ },
388
+ ],
389
+ },
390
+ ],
391
+ },
392
+ ],
393
+ };
394
+ await postJson(`${host}/api/public/otel/v1/traces`, {
395
+ method: 'POST',
396
+ headers: {
397
+ authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`,
398
+ 'content-type': 'application/json',
399
+ 'x-langfuse-ingestion-version': '4',
400
+ },
401
+ body: JSON.stringify(body),
402
+ });
403
+ }
404
+ async function forwardRemote(line) {
405
+ if (remoteDisabled())
406
+ return;
407
+ await Promise.allSettled([
408
+ sendPostHog(line),
409
+ sendSentry(line),
410
+ sendLangfuse(line),
411
+ ]);
412
+ }
413
+ function dispatchRemote(line) {
414
+ const task = forwardRemote(line).finally(() => pendingRemote.delete(task));
415
+ pendingRemote.add(task);
416
+ }
417
+ function telemetryInstanceId() {
418
+ const raw = (process.env.MVP_INSTANCE_ID ?? 'unknown').trim().toLowerCase();
419
+ return /^[a-z0-9_-]{1,32}$/.test(raw) ? raw : 'unknown';
420
+ }
421
+ function emitTelemetry(payload) {
422
+ try {
423
+ const event = cleanEvent(payload);
424
+ if (!event)
425
+ return;
426
+ const line = {
427
+ ts: new Date().toISOString(),
428
+ instance: telemetryInstanceId(),
429
+ ...event,
430
+ };
431
+ console.log(JSON.stringify(line));
432
+ dispatchRemote(line);
433
+ }
434
+ catch {
435
+ // Telemetry must never break the product.
436
+ }
437
+ }
438
+ /** Primarily for orderly shutdowns and deterministic tests. */
439
+ async function flushTelemetry() {
440
+ await Promise.allSettled([...pendingRemote]);
441
+ }