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,249 @@
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.compileCacheOverlap = compileCacheOverlap;
7
+ exports.releaseSeedDir = releaseSeedDir;
8
+ exports.enableWorkspaceCompileCache = enableWorkspaceCompileCache;
9
+ exports.activeCompileCacheDir = activeCompileCacheDir;
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_module_1 = __importDefault(require("node:module"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ /**
14
+ * This workspace's own Node compile cache, enabled before anything heavy loads.
15
+ *
16
+ * WHY IT IS HERE AND NOT IN cli.ts: cli.ts's own imports are resolved before any
17
+ * statement in it runs, so a call there would enable the cache AFTER the modules
18
+ * that dominate a cold start had already been compiled. This module is imported
19
+ * first for its side effect, and nothing heavy is imported above it.
20
+ *
21
+ * WHY IT IS PER WORKSPACE: the cache is a write. Two workspaces sharing one
22
+ * directory would be one workspace's engine creating files another workspace's
23
+ * engine owns, which on a host where each workspace runs as its own OS account is
24
+ * a cross-tenant write channel. The directory therefore comes from
25
+ * MVP_COMPILE_CACHE_DIR, which provision-renor-tenant.sh derives per workspace
26
+ * (`/var/lib/renor/<slug>/cache/compile`) rather than from a shared value. With the
27
+ * variable unset there is NO cache: nothing is derived from MVP_MEMORY_DIR.
28
+ *
29
+ * WHY IT IS NEVER IN A DATA TREE: a cache directory that overlaps the memory,
30
+ * history or auth trees is neither created nor enabled here (Node writes its
31
+ * entries at exit, so enabling first would leave them behind even though
32
+ * loadMvpConfig then refuses the start), because those trees are indexed,
33
+ * exported, or hold client secrets.
34
+ *
35
+ * WHY A FAILURE IS NOT FATAL: this is a speed optimisation, not a correctness
36
+ * input. Node's own contract for enableCompileCache is to report rather than
37
+ * throw, and the engine must start on a read-only or unwritable cache dir, on a
38
+ * Node build without compile-cache support, and with NODE_DISABLE_COMPILE_CACHE=1.
39
+ * A refusal is reported once, on stderr, and startup continues — measurably
40
+ * slower on some hosts, never wrong.
41
+ */
42
+ /** Reported once per process, and only when the cache was not enabled. */
43
+ let reported = false;
44
+ /**
45
+ * Why `dir` may not be a compile cache, or null when it may.
46
+ *
47
+ * ONE implementation, used twice: here, BEFORE the directory is created and the cache enabled
48
+ * (a refused start must leave nothing behind in a data tree: Node writes its entries at process
49
+ * exit, including the exit after a refusal), and by loadMvpConfig, which turns it into the
50
+ * startup refusal. Dependency-free on purpose: this module runs before anything heavy loads.
51
+ */
52
+ function compileCacheOverlap(dir, trees) {
53
+ const inside = (a, b) => a !== b && b.startsWith(a + node_path_1.default.sep);
54
+ const overlapsEither = (a, b) => a === b || inside(a, b) || inside(b, a);
55
+ const labelled = [
56
+ ['the fact memory directory (MVP_MEMORY_DIR)', trees.memoryDir],
57
+ ['the promoted history directory (MVP_HISTORY_DIR)', trees.historyDir],
58
+ ['the OAuth client registry (MVP_AUTH_STATE_PATH)', trees.authStatePath],
59
+ ];
60
+ for (const [label, tree] of labelled) {
61
+ if (tree && overlapsEither(dir, tree)) {
62
+ return `MVP_COMPILE_CACHE_DIR (${dir}) must not be equal to, contain, or sit inside ${label}. `
63
+ + 'It is a rebuildable cache of compiled code, not user data, and a cache inside a data tree '
64
+ + 'would be indexed, exported, or shipped with the workspace.';
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ function resolvedEnvPath(name) {
70
+ const raw = (process.env[name] ?? '').trim();
71
+ return raw === '' ? null : node_path_1.default.resolve(raw);
72
+ }
73
+ /**
74
+ * Why an EXISTING cache directory may not be used, or null. A cache is loaded as code, so it
75
+ * must be a real directory (not a symlink) that only this process's user can write: a
76
+ * group- or world-writable one lets another account plant what this engine compiles from.
77
+ */
78
+ function unsafeExistingDir(dir) {
79
+ let st;
80
+ try {
81
+ st = node_fs_1.default.lstatSync(dir);
82
+ }
83
+ catch (err) {
84
+ return `cannot inspect ${dir} (${err.code ?? 'error'})`;
85
+ }
86
+ if (st.isSymbolicLink())
87
+ return `${dir} is a symlink`;
88
+ if (!st.isDirectory())
89
+ return `${dir} is not a directory`;
90
+ if ((st.mode & 0o022) !== 0) {
91
+ return `${dir} is writable by group or others (mode ${(st.mode & 0o777).toString(8)})`;
92
+ }
93
+ if (typeof process.geteuid === 'function' && st.uid !== process.geteuid()) {
94
+ return `${dir} is owned by another user (uid ${st.uid})`;
95
+ }
96
+ return null;
97
+ }
98
+ function declined(reason) {
99
+ if (!reported) {
100
+ reported = true;
101
+ console.error(`[mvp] compile cache not enabled (${reason}); the engine starts normally without it`);
102
+ }
103
+ return { enabled: false, directory: null, reason, seeded: 0 };
104
+ }
105
+ /**
106
+ * The release's compile-cache seed, copied into this workspace's own (empty) cache.
107
+ *
108
+ * WHY: Node keys a cache entry by the file's path, so every release path starts a workspace's cache empty
109
+ * (provision-renor-tenant.sh empties it when the pin moves), and the first wake after each release compiled the whole
110
+ * module graph again: about 400 ms of CPU on the harness's small workspace, which a hosted unit's CPUQuota stretches to
111
+ * about twice that in wall time. deploy-remote.sh runs `dist/mvp/compileSeed.js` once per release, at the release's final
112
+ * path, into `<release>/.compile-seed`; the engine copies those entries into the cache it owns when that cache is empty.
113
+ *
114
+ * WHY A COPY AND NOT A SHARED CACHE: the per-workspace cache above is one account's own write. Pointing every workspace at
115
+ * the release's directory would make it a shared one (or a read of one whose owner is another account). The copy keeps the
116
+ * invariant: the engine only ever loads code from a directory it owns, which only it can write, and that the seed
117
+ * filled before the first start.
118
+ *
119
+ * WHAT IS TRUSTED: only a seed directory that root (or this very account) owns and no group or other can write, holding
120
+ * regular files, in the sub-directory Node names for this Node build and V8 flags (it ends in the owner's uid; the build
121
+ * account's uid is not this workspace's, so the name is compared without it). Anything else is not read. A failure is
122
+ * never fatal: the cache fills as it always did.
123
+ */
124
+ const SEED_ENTRY = /^[0-9a-f]{8}$/;
125
+ const MAX_SEED_ENTRIES = 4096;
126
+ const MAX_SEED_ENTRY_BYTES = 32 * 1024 * 1024;
127
+ /** The directory deploy-remote.sh seeds for the release this code runs from, unless MVP_COMPILE_SEED_DIR names another. */
128
+ function releaseSeedDir() {
129
+ const explicit = resolvedEnvPath('MVP_COMPILE_SEED_DIR');
130
+ return explicit ?? node_path_1.default.resolve(__dirname, '..', '..', '.compile-seed');
131
+ }
132
+ function trustedSeedDir(dir) {
133
+ let st;
134
+ try {
135
+ st = node_fs_1.default.lstatSync(dir);
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ if (!st.isDirectory() || st.isSymbolicLink() || (st.mode & 0o022) !== 0)
141
+ return false;
142
+ const me = typeof process.geteuid === 'function' ? process.geteuid() : st.uid;
143
+ return st.uid === 0 || st.uid === me;
144
+ }
145
+ /** `v22.23.2-x64-9de703df-1000` -> `v22.23.2-x64-9de703df`: Node's cache sub-directory without the uid it ends in. */
146
+ function withoutUid(name) {
147
+ const m = /^(v[0-9][\w.]*-[\w]+-[0-9a-f]+)-\d+$/.exec(name);
148
+ return m ? m[1] : null;
149
+ }
150
+ /**
151
+ * Copy the seed into `target` (the versioned directory Node is using) when it holds no entries. Returns how many entries
152
+ * were copied. An entry that is half written (a start killed during the copy) is rejected by Node's own checksum and compiled again.
153
+ */
154
+ function seedFromRelease(target) {
155
+ try {
156
+ const seedRoot = releaseSeedDir();
157
+ if (!trustedSeedDir(seedRoot))
158
+ return 0;
159
+ const want = withoutUid(node_path_1.default.basename(target));
160
+ if (want === null)
161
+ return 0;
162
+ const sub = node_fs_1.default.readdirSync(seedRoot).find(name => withoutUid(name) === want);
163
+ if (sub === undefined)
164
+ return 0;
165
+ const from = node_path_1.default.join(seedRoot, sub);
166
+ if (!trustedSeedDir(from))
167
+ return 0;
168
+ node_fs_1.default.mkdirSync(target, { recursive: true, mode: 0o700 });
169
+ if (node_fs_1.default.readdirSync(target).length > 0)
170
+ return 0; // this workspace already has its own entries: never mix
171
+ let copied = 0;
172
+ for (const entry of node_fs_1.default.readdirSync(from, { withFileTypes: true })) {
173
+ if (!entry.isFile() || !SEED_ENTRY.test(entry.name))
174
+ continue;
175
+ if (copied >= MAX_SEED_ENTRIES)
176
+ break;
177
+ const src = node_path_1.default.join(from, entry.name);
178
+ if (node_fs_1.default.statSync(src).size > MAX_SEED_ENTRY_BYTES)
179
+ continue;
180
+ // Straight to the final name: Node checks each entry's size and checksum before it uses one and compiles again when
181
+ // they do not match, so an entry a killed start left half written costs a compile, never a wrong result. A temporary
182
+ // name and a rename would add 30 ms of syscalls for 588 entries.
183
+ node_fs_1.default.copyFileSync(src, node_path_1.default.join(target, entry.name), node_fs_1.default.constants.COPYFILE_EXCL);
184
+ copied++;
185
+ }
186
+ return copied;
187
+ }
188
+ catch {
189
+ return 0; // a speed optimisation: whatever went wrong, the engine starts and fills its cache as it always did
190
+ }
191
+ }
192
+ /**
193
+ * Enable the cache and report what Node decided. Safe to call more than once;
194
+ * Node returns ALREADY_ENABLED and the first directory wins.
195
+ *
196
+ * Only an EXPLICIT MVP_COMPILE_CACHE_DIR enables a cache. There is deliberately no derived
197
+ * default: a directory worked out from MVP_MEMORY_DIR can land in a shared place (a memory
198
+ * directory of /tmp/x derives /tmp/cache/compile), and a shared compile cache is a cross-user
199
+ * write. The provisioner names a per-workspace directory in every env file it writes.
200
+ */
201
+ function enableWorkspaceCompileCache() {
202
+ const explicit = resolvedEnvPath('MVP_COMPILE_CACHE_DIR');
203
+ if (explicit === null) {
204
+ return { enabled: false, directory: null, reason: 'MVP_COMPILE_CACHE_DIR is not set', seeded: 0 };
205
+ }
206
+ // BEFORE mkdir and before enabling: a directory inside a data tree is skipped, never created.
207
+ // loadMvpConfig still refuses the start with the full message afterwards.
208
+ const overlap = compileCacheOverlap(explicit, {
209
+ memoryDir: resolvedEnvPath('MVP_MEMORY_DIR'),
210
+ historyDir: resolvedEnvPath('MVP_HISTORY_DIR'),
211
+ authStatePath: resolvedEnvPath('MVP_AUTH_STATE_PATH'),
212
+ });
213
+ if (overlap !== null) {
214
+ return { enabled: false, directory: null, reason: 'MVP_COMPILE_CACHE_DIR overlaps a data tree; no cache written', seeded: 0 };
215
+ }
216
+ if (typeof node_module_1.default.enableCompileCache !== 'function') {
217
+ return declined('this Node build has no module.enableCompileCache');
218
+ }
219
+ // Create it here rather than trusting it to exist: the engine is the thing that writes into
220
+ // it, and a 0700 directory it owns is the narrowest grant that lets it. mkdir with recursive
221
+ // ignores EEXIST, so this is idempotent. A failure is not fatal: the checks below report it.
222
+ try {
223
+ node_fs_1.default.mkdirSync(explicit, { recursive: true, mode: 0o700 });
224
+ }
225
+ catch {
226
+ // Reported through the checks below; nothing to add here.
227
+ }
228
+ const unsafe = unsafeExistingDir(explicit);
229
+ if (unsafe !== null)
230
+ return declined(unsafe);
231
+ const result = node_module_1.default.enableCompileCache(explicit);
232
+ const accepted = result.status === node_module_1.default.constants.compileCacheStatus.ENABLED
233
+ || result.status === node_module_1.default.constants.compileCacheStatus.ALREADY_ENABLED;
234
+ if (!accepted) {
235
+ return declined(result.status === node_module_1.default.constants.compileCacheStatus.DISABLED
236
+ ? 'disabled by NODE_DISABLE_COMPILE_CACHE'
237
+ : result.message ?? `status ${result.status}`);
238
+ }
239
+ const directory = result.directory ?? explicit;
240
+ return { enabled: true, directory, reason: null, seeded: seedFromRelease(node_module_1.default.getCompileCacheDir() ?? directory) };
241
+ }
242
+ /**
243
+ * The directory this process is actually using, or undefined when no cache is
244
+ * enabled. Read from Node rather than recomputed, so it reports the truth even
245
+ * when NODE_COMPILE_CACHE was set in the environment and won.
246
+ */
247
+ function activeCompileCacheDir() {
248
+ return node_module_1.default.getCompileCacheDir();
249
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Fills a compile-cache directory for THIS release, for deploy-remote.sh to run once at the release's final path:
4
+ //
5
+ // MVP_COMPILE_CACHE_DIR=<release>/.compile-seed node <release>/dist/mvp/compileSeed.js
6
+ //
7
+ // It does what cli.ts does before it serves: enables the cache first, then loads the modules the engine loads at start (the
8
+ // config, the server and everything under it, telemetry, the PostHog glue), and exits 0, which is when Node writes its entries.
9
+ // It starts no server, reads no workspace and needs no environment beyond the cache directory. The engine copies the result into
10
+ // each workspace's own cache on its first start of the release (compileCache.ts, seedFromRelease).
11
+ const compileCache_1 = require("./compileCache");
12
+ const report = (0, compileCache_1.enableWorkspaceCompileCache)();
13
+ try {
14
+ if (!report.enabled) {
15
+ console.error(`[compile-seed] not seeding: ${report.reason}`);
16
+ process.exit(2);
17
+ }
18
+ // Plain requires, after the cache is on, for the same reason cli.ts enables it first: these are the heavy modules.
19
+ for (const module of ['./config', './server', './telemetry', './posthogMcp'])
20
+ require(module);
21
+ console.log(`[compile-seed] loaded the startup modules into ${report.directory}`);
22
+ process.exit(0);
23
+ }
24
+ catch (err) {
25
+ console.error('[compile-seed] failed:', err instanceof Error ? err.message : err);
26
+ process.exit(1);
27
+ }
@@ -0,0 +1,288 @@
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.DEFAULT_LEDGER_REL = void 0;
7
+ exports.loadMvpConfig = loadMvpConfig;
8
+ const path_1 = __importDefault(require("path"));
9
+ const compileCache_1 = require("./compileCache");
10
+ exports.DEFAULT_LEDGER_REL = 'System/Memory Events Ledger.md';
11
+ /** Values printed in the documentation (and the commonest guesses): anyone who has read the README can sign in with them. */
12
+ const KNOWN_WEAK_OWNER_PASSWORDS = new Set([
13
+ 'replace-this-password', 'choose-a-long-secret', '<owner-secret>', 'owner-secret', 'your-password-here',
14
+ 'password', 'password1', 'password123', '12345678', '123456789', 'qwertyui', 'qwerty123', 'changeme', 'change-me', 'letmein1',
15
+ ]);
16
+ function weakOwnerPasswordReason(password) {
17
+ const lower = password.toLowerCase();
18
+ if (KNOWN_WEAK_OWNER_PASSWORDS.has(lower))
19
+ return 'it is an example value from the documentation or a very common password';
20
+ if (new Set(lower).size === 1)
21
+ return 'it is one character repeated';
22
+ return null;
23
+ }
24
+ function requiredEnv(name, hint) {
25
+ const value = process.env[name];
26
+ if (!value || value.trim() === '') {
27
+ throw new Error(`Missing required configuration ${name}: ${hint}`);
28
+ }
29
+ return value.trim();
30
+ }
31
+ /**
32
+ * Where this workspace keeps its Node compile cache.
33
+ *
34
+ * Three properties, each a correctness property rather than taste:
35
+ *
36
+ * - INSIDE this workspace's own state tree, named explicitly: provision-renor-tenant.sh writes
37
+ * `/var/lib/renor/<slug>/cache/compile` into every env file, and that whole tree is the unit's
38
+ * only ReadWritePaths entry. There is NO derived default (null when unset): a path worked out
39
+ * from the memory directory can land in a shared place such as /tmp/cache/compile.
40
+ * - NEVER inside a data tree. The memory directory is indexed and exported, the history directory
41
+ * is promoted transcripts, and the auth path holds client secrets; Node's compiled-code blobs
42
+ * belong in none of them. Overlap in EITHER direction throws.
43
+ * - NEVER shared. Overlap is checked against THIS workspace's trees, but a path pointing into a
44
+ * sibling workspace is not detectable from inside this process — so the provisioner derives
45
+ * the path per workspace instead of reading a shared configuration value, and refuses an env
46
+ * file that names anything else.
47
+ */
48
+ function resolveCompileCacheDir(memoryDir, historyDir, authStatePath) {
49
+ const raw = (process.env.MVP_COMPILE_CACHE_DIR ?? '').trim();
50
+ if (raw === '')
51
+ return null;
52
+ const dir = path_1.default.resolve(raw);
53
+ const refusal = (0, compileCache_1.compileCacheOverlap)(dir, { memoryDir, historyDir, authStatePath });
54
+ if (refusal !== null)
55
+ throw new Error(refusal);
56
+ return dir;
57
+ }
58
+ function loadMvpConfig() {
59
+ const memoryDir = requiredEnv('MVP_MEMORY_DIR', 'dedicated owner memory directory for this product (create a NEW folder, e.g. ~/memory-company-data)');
60
+ const port = Number(process.env.MVP_PORT ?? 3471);
61
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
62
+ throw new Error('MVP_PORT must be an integer between 1 and 65535');
63
+ }
64
+ const baseUrlRaw = requiredEnv('MVP_BASE_URL', 'the externally reachable base URL of this service, e.g. http://localhost:3471 for local tests or your public https URL for ChatGPT');
65
+ let baseUrl;
66
+ try {
67
+ baseUrl = new URL(baseUrlRaw);
68
+ }
69
+ catch {
70
+ throw new Error(`MVP_BASE_URL is not a valid URL: ${baseUrlRaw}`);
71
+ }
72
+ if (baseUrl.hash || baseUrl.search) {
73
+ throw new Error('MVP_BASE_URL must not contain a query or fragment');
74
+ }
75
+ // Owner identity: the ONLY account that can complete authorization.
76
+ const ownerEmail = requiredEnv('MVP_OWNER_EMAIL', 'the single owner login email; no other identity may authorize').toLowerCase();
77
+ const ownerPassword = requiredEnv('MVP_OWNER_PASSWORD', 'the owner login secret (env-only; never commit it to Git or write it into the memory directory)');
78
+ if (ownerPassword.length < 8) {
79
+ throw new Error('MVP_OWNER_PASSWORD must be at least 8 characters');
80
+ }
81
+ // The README's example is public: anyone who can reach the login page signs in with it, so refuse to start on it.
82
+ const weakReason = weakOwnerPasswordReason(ownerPassword);
83
+ if (weakReason) {
84
+ throw new Error(`MVP_OWNER_PASSWORD is too weak: ${weakReason}. Choose your own secret, for example: export MVP_OWNER_PASSWORD="$(openssl rand -base64 18)"`);
85
+ }
86
+ const accessTokenTtlSeconds = Number(process.env.MVP_ACCESS_TOKEN_TTL_SECONDS ?? 3600);
87
+ if (!Number.isFinite(accessTokenTtlSeconds) || accessTokenTtlSeconds < 60) {
88
+ throw new Error('MVP_ACCESS_TOKEN_TTL_SECONDS must be at least 60');
89
+ }
90
+ // AUTH-B1: exact callback-URI allowlist for DCR. Deny by default: unset or
91
+ // empty means NO client may register until the owner names exact callbacks.
92
+ const allowedCallbackUris = (process.env.MVP_ALLOWED_CALLBACK_URIS ?? '')
93
+ .split(',').map(s => s.trim()).filter(Boolean);
94
+ for (const uri of allowedCallbackUris) {
95
+ try {
96
+ new URL(uri);
97
+ }
98
+ catch {
99
+ throw new Error(`MVP_ALLOWED_CALLBACK_URIS contains an invalid URL: ${uri}`);
100
+ }
101
+ if (/[\s]/.test(uri)) {
102
+ throw new Error('MVP_ALLOWED_CALLBACK_URIS entries must be exact URIs without whitespace');
103
+ }
104
+ }
105
+ // AUTH-B3: bounded failed-login limiter knobs (operator-tunable, sane defaults).
106
+ const loginMaxFailures = Number(process.env.MVP_LOGIN_MAX_FAILURES ?? 5);
107
+ if (!Number.isInteger(loginMaxFailures) || loginMaxFailures < 1 || loginMaxFailures > 100) {
108
+ throw new Error('MVP_LOGIN_MAX_FAILURES must be an integer between 1 and 100');
109
+ }
110
+ const loginLockoutMs = Number(process.env.MVP_LOGIN_LOCKOUT_MS ?? 30_000);
111
+ if (!Number.isInteger(loginLockoutMs) || loginLockoutMs < 1 || loginLockoutMs > 3_600_000) {
112
+ throw new Error('MVP_LOGIN_LOCKOUT_MS must be an integer between 1 and 3600000');
113
+ }
114
+ const memoryDirResolved = path_1.default.resolve(memoryDir);
115
+ // CLIENT-PERSISTENCE: explicit private OAuth client-registry path. The
116
+ // registry holds registered client metadata (incl. client secrets), so it
117
+ // must live outside the memory and history trees in both directions.
118
+ const authStatePathRaw = requiredEnv('MVP_AUTH_STATE_PATH', 'private path of the persistent OAuth client registry, e.g. /path/to/private-runtime/auth/clients.json '
119
+ + '(file inside a NEW owner-only 0700 directory OUTSIDE the memory/history/export directories; never in Git)');
120
+ const authStatePath = path_1.default.resolve(authStatePathRaw);
121
+ // SSE-CHATGPT: transport mode. Default stays JSON (stateless) — the exact
122
+ // behavior every existing test pins. 'false'/'0' opts into SSE responses.
123
+ const gatewaySecret = (process.env.MVP_GATEWAY_SECRET ?? '').trim();
124
+ const mcpJsonRaw = (process.env.MVP_MCP_JSON ?? 'true').trim().toLowerCase();
125
+ if (mcpJsonRaw !== 'true' && mcpJsonRaw !== 'false' && mcpJsonRaw !== '1' && mcpJsonRaw !== '0') {
126
+ throw new Error('MVP_MCP_JSON must be true or false');
127
+ }
128
+ const mcpJsonResponse = mcpJsonRaw === 'true' || mcpJsonRaw === '1';
129
+ const mcpMaxSessions = Number(process.env.MVP_MCP_MAX_SESSIONS ?? 100);
130
+ // Each retained session holds a full MCP server (about 0.3 MB measured), so the ceiling is 1,000: 10,000 would
131
+ // extrapolate to roughly 3 GB of heap, past Node's default limit.
132
+ if (!Number.isInteger(mcpMaxSessions) || mcpMaxSessions < 1) {
133
+ throw new Error('MVP_MCP_MAX_SESSIONS must be an integer of at least 1');
134
+ }
135
+ if (mcpMaxSessions > 1_000) {
136
+ throw new Error(`MVP_MCP_MAX_SESSIONS is ${mcpMaxSessions}; the largest accepted value is 1000 (each session costs about 0.3 MB of heap)`);
137
+ }
138
+ const mcpSessionTtlMs = Number(process.env.MVP_MCP_SESSION_TTL_MS ?? 3_600_000);
139
+ if (!Number.isInteger(mcpSessionTtlMs) || mcpSessionTtlMs < 1_000 || mcpSessionTtlMs > 86_400_000) {
140
+ throw new Error('MVP_MCP_SESSION_TTL_MS must be an integer between 1000 and 86400000');
141
+ }
142
+ const embeddingRaw = (process.env.MVP_EMBEDDING_PROVIDER ?? process.env.EMBEDDING_PROVIDER ?? 'none').trim().toLowerCase();
143
+ if (embeddingRaw !== 'none' && embeddingRaw !== 'local' && embeddingRaw !== 'openai' && embeddingRaw !== 'vertex') {
144
+ throw new Error('MVP_EMBEDDING_PROVIDER must be none, local, openai, or vertex');
145
+ }
146
+ const embeddingProvider = embeddingRaw;
147
+ let embeddingApiKey;
148
+ let embeddingModel;
149
+ let embeddingBaseURL;
150
+ let embeddingDims;
151
+ let vertexKeyFile;
152
+ let vertexProject;
153
+ let vertexLocation;
154
+ if (embeddingProvider === 'openai') {
155
+ const key = (process.env.MVP_EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY ?? '').trim();
156
+ if (!key)
157
+ throw new Error('MVP_EMBEDDING_API_KEY (or OPENAI_API_KEY) is required when MVP_EMBEDDING_PROVIDER=openai');
158
+ embeddingApiKey = key;
159
+ embeddingModel = (process.env.MVP_EMBEDDING_MODEL ?? process.env.EMBEDDING_MODEL ?? 'voyage-4-large').trim();
160
+ embeddingBaseURL = (process.env.MVP_EMBEDDING_BASE_URL ?? process.env.EMBEDDING_BASE_URL ?? 'https://api.voyageai.com/v1').trim();
161
+ const dimsRaw = process.env.MVP_EMBEDDING_DIMS ?? process.env.EMBEDDING_DIMS ?? '1024';
162
+ embeddingDims = Number(dimsRaw);
163
+ if (!Number.isInteger(embeddingDims) || embeddingDims < 8 || embeddingDims > 8192) {
164
+ throw new Error('MVP_EMBEDDING_DIMS must be an integer between 8 and 8192');
165
+ }
166
+ }
167
+ else if (embeddingProvider === 'vertex') {
168
+ const keyFile = (process.env.MVP_VERTEX_KEY_FILE
169
+ ?? process.env.GOOGLE_APPLICATION_CREDENTIALS
170
+ ?? process.env.RENOR_EVAL_VERTEX_KEY_FILE
171
+ ?? '').trim();
172
+ if (!keyFile) {
173
+ throw new Error('MVP_VERTEX_KEY_FILE (or GOOGLE_APPLICATION_CREDENTIALS) is required when MVP_EMBEDDING_PROVIDER=vertex');
174
+ }
175
+ vertexKeyFile = path_1.default.resolve(keyFile);
176
+ vertexProject = (process.env.MVP_VERTEX_PROJECT
177
+ ?? process.env.RENOR_EVAL_VERTEX_PROJECT
178
+ ?? '').trim();
179
+ if (!/^[a-z0-9-]+$/.test(vertexProject)) {
180
+ throw new Error('MVP_VERTEX_PROJECT (or RENOR_EVAL_VERTEX_PROJECT) is required when MVP_EMBEDDING_PROVIDER=vertex');
181
+ }
182
+ vertexLocation = (process.env.MVP_VERTEX_LOCATION
183
+ ?? process.env.RENOR_EVAL_VERTEX_LOCATION
184
+ ?? '').trim();
185
+ if (!vertexLocation) {
186
+ throw new Error('MVP_VERTEX_LOCATION (or RENOR_EVAL_VERTEX_LOCATION) is required when MVP_EMBEDDING_PROVIDER=vertex');
187
+ }
188
+ if (!/^[a-z0-9-]+$/.test(vertexLocation)) {
189
+ throw new Error('MVP_VERTEX_LOCATION must be a valid GCP location like europe-west4');
190
+ }
191
+ embeddingModel = (process.env.MVP_EMBEDDING_MODEL ?? process.env.EMBEDDING_MODEL ?? process.env.RENOR_EVAL_MODEL ?? 'gemini-embedding-001').trim();
192
+ const dimsRaw = process.env.MVP_EMBEDDING_DIMS ?? process.env.EMBEDDING_DIMS ?? process.env.RENOR_EVAL_EMBEDDING_DIMS ?? '768';
193
+ embeddingDims = Number(dimsRaw);
194
+ if (!Number.isInteger(embeddingDims) || ![768, 1536, 3072].includes(embeddingDims)) {
195
+ throw new Error('MVP_EMBEDDING_DIMS for vertex must be 768, 1536, or 3072');
196
+ }
197
+ }
198
+ else if (embeddingProvider === 'local') {
199
+ embeddingModel = (process.env.MVP_EMBEDDING_MODEL ?? process.env.EMBEDDING_MODEL ?? 'Xenova/multilingual-e5-base').trim();
200
+ const dimsRaw = process.env.MVP_EMBEDDING_DIMS ?? process.env.EMBEDDING_DIMS;
201
+ if (dimsRaw) {
202
+ embeddingDims = Number(dimsRaw);
203
+ if (!Number.isInteger(embeddingDims) || embeddingDims < 8 || embeddingDims > 8192) {
204
+ throw new Error('MVP_EMBEDDING_DIMS must be an integer between 8 and 8192');
205
+ }
206
+ }
207
+ }
208
+ let explicitDbPath = null;
209
+ const rawDb = process.env.MVP_DB_PATH;
210
+ if (rawDb && rawDb.trim() !== '') {
211
+ explicitDbPath = path_1.default.resolve(rawDb.trim());
212
+ if (explicitDbPath.endsWith('.md')) {
213
+ throw new Error('MVP_DB_PATH must not be a Markdown file');
214
+ }
215
+ }
216
+ // On-demand (RENOR_ON_DEMAND): the one switch, off unless explicitly on. Only enable it
217
+ // for a workspace whose clients reach it through the gateway, with the gateway's own
218
+ // RENOR_ON_DEMAND on: nothing else starts a sleeping engine.
219
+ const onDemandRaw = (process.env.RENOR_ON_DEMAND ?? '').trim().toLowerCase();
220
+ if (!['', '0', 'false', '1', 'true'].includes(onDemandRaw)) {
221
+ throw new Error('RENOR_ON_DEMAND must be 1/true or 0/false');
222
+ }
223
+ let idleExitMs = null;
224
+ if (onDemandRaw === '1' || onDemandRaw === 'true') {
225
+ const idleSeconds = Number(process.env.RENOR_ON_DEMAND_IDLE_SECONDS ?? 600);
226
+ if (!Number.isInteger(idleSeconds) || idleSeconds < 1 || idleSeconds > 86_400) {
227
+ throw new Error('RENOR_ON_DEMAND_IDLE_SECONDS must be an integer between 1 and 86400');
228
+ }
229
+ idleExitMs = idleSeconds * 1000;
230
+ }
231
+ const overlaps = (a, b) => a === b || a.startsWith(b + path_1.default.sep);
232
+ if (overlaps(authStatePath, memoryDirResolved)) {
233
+ throw new Error('MVP_AUTH_STATE_PATH must not be inside the fact memory directory (MVP_MEMORY_DIR)');
234
+ }
235
+ if (authStatePath.endsWith('.md')) {
236
+ throw new Error('MVP_AUTH_STATE_PATH must not be a Markdown file');
237
+ }
238
+ // Promoted-history directory: optional, explicit, and strictly disjoint
239
+ // from the fact memory directory (no overlap in either direction).
240
+ let historyDir = null;
241
+ const rawHistoryDir = process.env.MVP_HISTORY_DIR;
242
+ if (rawHistoryDir && rawHistoryDir.trim() !== '') {
243
+ historyDir = path_1.default.resolve(rawHistoryDir.trim());
244
+ const inside = (a, b) => a !== b && b.startsWith(a + path_1.default.sep);
245
+ if (historyDir === memoryDirResolved
246
+ || inside(historyDir, memoryDirResolved)
247
+ || inside(memoryDirResolved, historyDir)) {
248
+ throw new Error('MVP_HISTORY_DIR must not be equal to or overlap the fact memory directory (MVP_MEMORY_DIR)');
249
+ }
250
+ if (overlaps(authStatePath, historyDir) || overlaps(historyDir, authStatePath)) {
251
+ throw new Error('MVP_AUTH_STATE_PATH must not be equal to or overlap the promoted history directory (MVP_HISTORY_DIR)');
252
+ }
253
+ }
254
+ // This workspace's own compile cache. A wrong location is REFUSED, not repaired: silently
255
+ // relocating it would hide the misconfiguration that, in a shared directory, is precisely the
256
+ // cross-tenant write this exists to prevent.
257
+ const compileCacheDir = resolveCompileCacheDir(memoryDirResolved, historyDir, authStatePath);
258
+ return {
259
+ memoryDir: memoryDirResolved,
260
+ dbPath: explicitDbPath ?? path_1.default.join(path_1.default.resolve(memoryDir), '.memory-index', 'index.db'),
261
+ ledgerRelPath: process.env.MVP_LEDGER_REL ?? exports.DEFAULT_LEDGER_REL,
262
+ baseUrl,
263
+ port,
264
+ ownerEmail,
265
+ ownerPassword,
266
+ accessTokenTtlSeconds,
267
+ gatewaySecret,
268
+ allowedCallbackUris,
269
+ loginMaxFailures,
270
+ loginLockoutMs,
271
+ historyDir,
272
+ authStatePath,
273
+ mcpJsonResponse,
274
+ mcpMaxSessions,
275
+ mcpSessionTtlMs,
276
+ embeddingProvider,
277
+ embeddingApiKey,
278
+ embeddingModel,
279
+ embeddingBaseURL,
280
+ embeddingDims,
281
+ vertexKeyFile,
282
+ vertexProject,
283
+ vertexLocation,
284
+ explicitDbPath,
285
+ idleExitMs,
286
+ compileCacheDir,
287
+ };
288
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /**
3
+ * Which Renor an agent is talking to, said once and whenever something is wrong.
4
+ *
5
+ * Agents could not tell that they were connected to the wrong server, or that a connection had silently dropped:
6
+ * absence was silent. So the first tool reply of an MCP session carries one short status block, and so does every
7
+ * error a tool returns, with a sentence on how to reconnect. A later successful reply of the same session carries
8
+ * nothing, so the status costs a few dozen tokens once per session, not a line on every reply.
9
+ *
10
+ * The status is an extra trailing text block on the reply. The block a tool always returned is untouched, so
11
+ * nothing that reads a reply by its first block changes.
12
+ *
13
+ * It is on only for a hosted workspace (the provisioner writes MVP_INSTANCE_ID, the workspace's slug, into every
14
+ * tenant's environment); an engine run any other way answers as it always did.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ConnectionAnnouncer = exports.HOSTED_MCP_URL = void 0;
18
+ exports.connectionStatusFromEnv = connectionStatusFromEnv;
19
+ exports.connectionStatusBlock = connectionStatusBlock;
20
+ /** The one public MCP URL every hosted workspace is reached through (the gateway routes it to the workspace). */
21
+ exports.HOSTED_MCP_URL = 'https://mcp.renor.cloud/mcp';
22
+ /**
23
+ * The status of the hosted workspace this process serves, or null when it is not one (no MVP_INSTANCE_ID, or
24
+ * RENOR_CONNECTION_STATUS=0, the way to switch it off).
25
+ */
26
+ function connectionStatusFromEnv(env) {
27
+ if ((env.RENOR_CONNECTION_STATUS ?? '').trim() === '0')
28
+ return null;
29
+ const workspace = (env.MVP_INSTANCE_ID ?? '').trim().toLowerCase();
30
+ // The slug shape the control plane issues; anything else is not a hosted workspace's name.
31
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(workspace))
32
+ return null;
33
+ return { workspace, url: exports.HOSTED_MCP_URL };
34
+ }
35
+ /** What an agent is told on its first reply: where it is, and what to do if that is not where it expected to be. */
36
+ function firstSentence(status) {
37
+ return `Connected to Renor's hosted workspace "${status.workspace}" at ${status.url}; if that is not the workspace you expect, tell the user.`;
38
+ }
39
+ /** What an agent is told on an error: where it is, and how to connect again. */
40
+ function errorSentence(status) {
41
+ return `Renor hosted workspace "${status.workspace}" at ${status.url}. If Renor stops answering or refuses you, reconnect: sign in again through your client's MCP settings for ${status.url}.`;
42
+ }
43
+ /** The one-line text block: a stable machine-readable field, and one short sentence. */
44
+ function connectionStatusBlock(status, kind) {
45
+ const message = kind === 'first' ? firstSentence(status) : errorSentence(status);
46
+ return {
47
+ type: 'text',
48
+ text: JSON.stringify({ renor_connection: { hosted: true, workspace: status.workspace, url: status.url }, message }),
49
+ };
50
+ }
51
+ /**
52
+ * Decides, per MCP session, which replies carry the status. One of these per session server: the first tool reply
53
+ * takes the announcement (a session that has none to announce, because it has no session to begin, never does), and
54
+ * an error always carries it.
55
+ */
56
+ class ConnectionAnnouncer {
57
+ status;
58
+ announced;
59
+ constructor(status, announceFirst) {
60
+ this.status = status;
61
+ this.announced = !announceFirst;
62
+ }
63
+ /** The reply, with the status block appended where it belongs, or the reply itself where it does not. */
64
+ decorate(result, first) {
65
+ if (this.status === null || !Array.isArray(result?.content))
66
+ return result;
67
+ const error = result.isError === true;
68
+ if (!error && !first)
69
+ return result;
70
+ return { ...result, content: [...result.content, connectionStatusBlock(this.status, error ? 'error' : 'first')] };
71
+ }
72
+ /** Called when a tool call starts: true for exactly one call in the session, the first. */
73
+ takeFirst() {
74
+ if (this.announced)
75
+ return false;
76
+ this.announced = true;
77
+ return true;
78
+ }
79
+ }
80
+ exports.ConnectionAnnouncer = ConnectionAnnouncer;