overmind-mcp 2.8.43 → 2.8.45

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.
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawn } from 'child_process';
4
- import { exec } from 'child_process';
4
+ import { exec, execSync } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import { CONFIG, resolveConfigPath, getWorkspaceDir, getAgentHermesHome, getAgentOvermindHome, getSharedHermesHome } from '../lib/config.js';
7
7
  import { getLastSessionId, saveSessionId } from '../lib/sessions.js';
@@ -18,34 +18,55 @@ const logger = pino({ name: 'NousHermesRunner' });
18
18
  // orphelin. On utilise taskkill /F /T pour propager le kill au sous-arbre complet.
19
19
  const killProcessTree = (child) => {
20
20
  return new Promise((resolve) => {
21
- if (!child || child.exitCode !== null || child.killed) {
21
+ if (!child) {
22
+ logger.debug('[KILL] No child process reference provided.');
22
23
  resolve();
23
24
  return;
24
25
  }
26
+ if (child.exitCode !== null || child.killed) {
27
+ logger.debug({ pid: child.pid, exitCode: child.exitCode, killed: child.killed }, '[KILL] Process is already dead or marked killed.');
28
+ resolve();
29
+ return;
30
+ }
31
+ logger.info({ pid: child.pid }, '[KILL] Initiating process tree termination...');
25
32
  let settled = false;
26
33
  const finish = () => {
27
34
  if (settled)
28
35
  return;
29
36
  settled = true;
37
+ logger.debug({ pid: child.pid }, '[KILL] Process tree termination sequence completed.');
30
38
  resolve();
31
39
  };
32
40
  child.once('exit', finish);
33
- if (process.platform === 'win32' && child.pid) {
34
- exec(`taskkill /F /T /PID ${child.pid}`, () => {
35
- // taskkill peut échouer si le process est déjà mort
41
+ if (process.platform === 'win32' && child.pid && typeof child.pid === 'number' && child.pid > 0) {
42
+ const cmd = `taskkill /F /T /PID ${child.pid}`;
43
+ logger.debug({ cmd }, '[KILL] Executing Windows taskkill...');
44
+ exec(cmd, (err, stdout, stderr) => {
45
+ if (err) {
46
+ logger.debug({ err, stderr }, '[KILL] taskkill failed or process was already dead.');
47
+ }
48
+ else {
49
+ logger.debug({ stdout }, '[KILL] taskkill completed successfully.');
50
+ }
36
51
  });
37
52
  }
38
53
  else {
39
54
  try {
55
+ logger.debug({ pid: child.pid }, '[KILL] Dispatched SIGTERM signal.');
40
56
  child.kill('SIGTERM');
41
57
  }
42
- catch { /* ignored */ }
58
+ catch (e) {
59
+ logger.debug({ pid: child.pid, error: e }, '[KILL] SIGTERM dispatch failed.');
60
+ }
43
61
  setTimeout(() => {
44
62
  if (child.exitCode === null && !child.killed) {
45
63
  try {
64
+ logger.warn({ pid: child.pid }, '[KILL] SIGTERM ignored. Escalating to SIGKILL...');
46
65
  child.kill('SIGKILL');
47
66
  }
48
- catch { /* ignored */ }
67
+ catch (e) {
68
+ logger.debug({ pid: child.pid, error: e }, '[KILL] SIGKILL dispatch failed.');
69
+ }
49
70
  }
50
71
  }, 2000);
51
72
  }
@@ -95,30 +116,33 @@ async function findHermesBinary() {
95
116
  // Not found in PATH
96
117
  }
97
118
  // 3. Platform-specific paths
98
- const platformPaths = isWin
99
- ? [
100
- // Hermes venv (Nous Research install) — PRIORITÉ haute (v0.13.0, supporte -z)
101
- path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes-agent', 'venv', 'Scripts', 'hermes.exe'),
102
- // Officiel installer Windows (install.ps1) — chemin natif
103
- path.join(process.env.LOCALAPPDATA || '', 'hermes', 'bin', 'hermes.exe'),
104
- path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes.exe'),
105
- // Fallback installations via pip (legacy)
106
- path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python312', 'Scripts', 'hermes.exe'),
107
- path.join(process.env.APPDATA || '', 'Python', 'Python312', 'Scripts', 'hermes.exe'),
108
- path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python311', 'Scripts', 'hermes.exe'),
109
- path.join(process.env.APPDATA || '', 'Python', 'Python311', 'Scripts', 'hermes.exe'),
110
- 'C:\\Python312\\Scripts\\hermes.exe',
111
- 'C:\\Python311\\Scripts\\hermes.exe',
112
- 'C:\\Program Files\\Hermes\\hermes.exe',
113
- ]
114
- : [
115
- path.join(process.env.HOME || '', '.local', 'bin', 'hermes'),
116
- path.join(process.env.HOME || '', 'miniconda3', 'bin', 'hermes'),
117
- path.join(process.env.HOME || '', 'anaconda3', 'bin', 'hermes'),
118
- '/usr/local/bin/hermes',
119
- '/usr/bin/hermes',
120
- '/opt/homebrew/bin/hermes',
121
- ];
119
+ const platformPaths = [];
120
+ if (isWin) {
121
+ platformPaths.push(
122
+ // Hermes venv (Nous Research install) PRIORITÉ haute (v0.13.0, supporte -z)
123
+ path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes-agent', 'venv', 'Scripts', 'hermes.exe'),
124
+ // Officiel installer Windows (install.ps1) — chemin natif
125
+ path.join(process.env.LOCALAPPDATA || '', 'hermes', 'bin', 'hermes.exe'), path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes.exe'));
126
+ // Dynamically scan for Python versions in LOCALAPPDATA
127
+ const programsPython = path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python');
128
+ if (fs.existsSync(programsPython)) {
129
+ try {
130
+ const dirs = fs.readdirSync(programsPython);
131
+ for (const dir of dirs) {
132
+ if (dir.toLowerCase().startsWith('python')) {
133
+ platformPaths.push(path.join(programsPython, dir, 'Scripts', 'hermes.exe'));
134
+ }
135
+ }
136
+ }
137
+ catch { /* ignored */ }
138
+ }
139
+ platformPaths.push(
140
+ // Fallback installations via pip (legacy)
141
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python312', 'Scripts', 'hermes.exe'), path.join(process.env.APPDATA || '', 'Python', 'Python312', 'Scripts', 'hermes.exe'), path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python311', 'Scripts', 'hermes.exe'), path.join(process.env.APPDATA || '', 'Python', 'Python311', 'Scripts', 'hermes.exe'), 'C:\\Python312\\Scripts\\hermes.exe', 'C:\\Python311\\Scripts\\hermes.exe', 'C:\\Program Files\\Hermes\\hermes.exe');
142
+ }
143
+ else {
144
+ platformPaths.push(path.join(process.env.HOME || '', '.local', 'bin', 'hermes'), path.join(process.env.HOME || '', 'miniconda3', 'bin', 'hermes'), path.join(process.env.HOME || '', 'anaconda3', 'bin', 'hermes'), '/usr/local/bin/hermes', '/usr/bin/hermes', '/opt/homebrew/bin/hermes');
145
+ }
122
146
  for (const p of platformPaths) {
123
147
  if (fs.existsSync(p)) {
124
148
  logger.info({ path: p }, 'Found hermes at platform path');
@@ -148,21 +172,111 @@ async function findHermesBinary() {
148
172
  return 'hermes';
149
173
  }
150
174
  /**
151
- * NousHermesRunner — Runner polyglote pour Hermes Agent (Overmind 2.8.27+).
175
+ * NousHermesRunner — Runner Hermes Agent pour Overmind (v2.8.45+).
176
+ *
177
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
178
+ * ║ ⭐ ARCHITECTURE CREDENTIALS HERMES — LIRE CECI EN PREMIER ║
179
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
180
+ * ║ ║
181
+ * ║ Les credentials Hermes sont dans LE DOSSIER NATIF HERMES UNIQUEMENT : ║
182
+ * ║ ║
183
+ * ║ <HERMES_HOME>/agents/<name>/settings.json ║
184
+ * ║ ║
185
+ * ║ Sur Linux (npm -g) : /home/demon/.overmind/hermes/agents/<name>/ ║
186
+ * ║ Sur Windows (dev) : Workflow/.overmind/hermes/agents/<name>/ ║
187
+ * ║ ║
188
+ * ║ ❌ .claude/settings_<name>.json → CLAUDE CODE / KILO SEULEMENT ║
189
+ * ║ ❌ NousHermesRunner NE LIT JAMAIS depuis .claude/ ║
190
+ * ║ ❌ Ne pas éditer hermes/agents/<name>/settings.json manuellement ║
191
+ * ║ entre les runs — le runner le met à jour automatiquement. ║
192
+ * ║ ║
193
+ * ║ FORMAT du settings.json Hermes : ║
194
+ * ║ { ║
195
+ * ║ "env": { ║
196
+ * ║ "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ← token MiniMax / Z.AI ║
197
+ * ║ "ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ║
198
+ * ║ "ANTHROPIC_MODEL": "MiniMax-M3", ║
199
+ * ║ "ANTHROPIC_PROVIDER": "minimax-cn", ║
200
+ * ║ "MINIMAX_CN_API_KEY": "sk-cp-...", ← injecté auto par le runner ║
201
+ * ║ "MINIMAX_CN_BASE_URL": "https://api.minimaxi.com/anthropic" ║
202
+ * ║ }, ║
203
+ * ║ "enableAllProjectMcpServers": false, ║
204
+ * ║ "enabledMcpjsonServers": ["memory", "discord", "postgres"], ║
205
+ * ║ "agent": "<name>", ║
206
+ * ║ "runner": "hermes" ║
207
+ * ║ } ║
208
+ * ║ ║
209
+ * ║ Supporte l'interpolation $VAR (ex: "$ANTHROPIC_AUTH_TOKEN_1") ║
210
+ * ║ résolue depuis process.env au moment du spawn. ║
211
+ * ║ ║
212
+ * ║ HERMES_HOME résolu dans l'ordre : ║
213
+ * ║ 1. OVERMIND_HERMES_HOME (env var explicite, ex: systemd EnvironmentFile) ║
214
+ * ║ 2. <OVERMIND_WORKSPACE>/.overmind/hermes/ (dev local) ║
215
+ * ║ 3. ~/.overmind/hermes/ (Linux) / %LOCALAPPDATA%/overmind/hermes/ (Win) ║
216
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
152
217
  *
153
- * • Providers : OpenAI, MiniMax GLOBAL/CN, Zhipu/GLM, Mistral, NVIDIA NIM, OpenRouter (embeddings only)
154
- * • Lit settings_<agent>.json + .mcp.<agent>.json depuis .claude/ comme les autres runners
155
- * • Interpolation $VAR et ${VAR} sur tout settings + mcp config (via envUtils, regex fix 2.8.25)
156
- * • Subtilisation 3-pass (Pass 1: settings-explicit, Pass A: prefer provider-specific,
157
- * Pass B: re-map generic key, Pass C: rare fallback) — see hermesTokenResolver.ts
158
- * • CN/GLOBAL disambiguation for sk-cp-* via ANTHROPIC_BASE_URL (URL wins)
159
- * • OVERMIND_MINIMAX_DEFAULT=cn|global|auto for setups where all MiniMax tokens are CN
160
- * • HERMES_HOME resolved via getAgentHermesHome() — deterministic across cwd (2.8.27+)
161
- * Priority: OVERMIND_AGENT_HOME > legacy workspace > $HOME/.overmind/hermes/agent_<name>/
162
- * • auth.json credential_pool is PRUNED every run (keep version+oauth, drop stale creds)
163
- * to prevent Hermes from picking an exhausted bucket from a previous provider config
164
- * • HOME/USERPROFILE propagated to spawned Hermes so ~/.hermes lookups resolve canonically
218
+ * • Providers supportés : MiniMax CN/GLOBAL, Z.AI/GLM, Mistral, OpenAI, NVIDIA NIM
219
+ * • OpenRouter = embeddings UNIQUEMENT (bloqué pour LLM inference)
220
+ * • auth.json purgé à chaque run (évite les credentials stale de l'ancien provider)
221
+ * • HOME/USERPROFILE propagé au process Hermes pour résolution ~/.hermes canonique
222
+ * Voir hermesTokenResolver.ts pour le 3-pass token detection (sk-cp-/32hex/sk-ant-)
165
223
  */
224
+ function filterConfigYaml(sourceYamlPath, allowedServers) {
225
+ try {
226
+ if (!fs.existsSync(sourceYamlPath))
227
+ return 'mcp_servers: {}\n';
228
+ const yamlText = fs.readFileSync(sourceYamlPath, 'utf8');
229
+ const lines = yamlText.split(/\r?\n/);
230
+ let inMcpServers = false;
231
+ let currentServerName = '';
232
+ const serverBlocks = {};
233
+ const headerLines = [];
234
+ for (const line of lines) {
235
+ const trimmed = line.trim();
236
+ if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
237
+ inMcpServers = true;
238
+ headerLines.push(line);
239
+ continue;
240
+ }
241
+ if (inMcpServers) {
242
+ const indentMatch = line.match(/^(\s+)/);
243
+ if (!indentMatch) {
244
+ inMcpServers = false;
245
+ headerLines.push(line);
246
+ continue;
247
+ }
248
+ const indent = indentMatch[1].length;
249
+ const keyMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*:/);
250
+ if (keyMatch && indent === 2) {
251
+ currentServerName = keyMatch[1];
252
+ serverBlocks[currentServerName] = [line];
253
+ }
254
+ else if (currentServerName) {
255
+ serverBlocks[currentServerName].push(line);
256
+ }
257
+ }
258
+ else {
259
+ headerLines.push(line);
260
+ }
261
+ }
262
+ let result = '';
263
+ for (const line of headerLines) {
264
+ result += line + '\n';
265
+ if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
266
+ for (const srv of allowedServers) {
267
+ if (serverBlocks[srv]) {
268
+ result += serverBlocks[srv].join('\n') + '\n';
269
+ }
270
+ }
271
+ }
272
+ }
273
+ return result;
274
+ }
275
+ catch (err) {
276
+ logger.error({ sourceYamlPath, error: err }, '[YAML_FILTER] Unexpected failure while filtering config.yaml.');
277
+ return 'mcp_servers: {}\n';
278
+ }
279
+ }
166
280
  export class NousHermesRunner {
167
281
  timeoutMs;
168
282
  tempFiles = [];
@@ -171,20 +285,22 @@ export class NousHermesRunner {
171
285
  this.timeoutMs = CONFIG.TIMEOUT_MS || 900000; // 15 min default
172
286
  }
173
287
  cleanupTempFiles() {
288
+ logger.debug({ count: this.tempFiles.length }, '[CLEANUP] Cleaning up temporary run files.');
174
289
  for (const tempFile of this.tempFiles) {
175
290
  try {
176
291
  if (fs.existsSync(tempFile)) {
177
292
  fs.unlinkSync(tempFile);
178
- logger.debug({ tempFile }, 'Cleaned up temp file');
293
+ logger.debug({ tempFile }, '[CLEANUP] Cleaned up temp file');
179
294
  }
180
295
  }
181
296
  catch (err) {
182
- logger.warn({ tempFile, error: err }, 'Failed to cleanup temp file');
297
+ logger.warn({ tempFile, error: err }, '[CLEANUP] Failed to cleanup temp file');
183
298
  }
184
299
  }
185
300
  this.tempFiles = [];
186
301
  }
187
302
  async runAgent(options) {
303
+ logger.info({ agentName: options.agentName, model: options.model, sessionId: options.sessionId }, '[RUN_AGENT] Initiating runAgent entrypoint.');
188
304
  try {
189
305
  const result = await withSpan('hermes.runAgent', async (span) => {
190
306
  span.setAttribute('agentName', options.agentName || '');
@@ -198,13 +314,14 @@ export class NousHermesRunner {
198
314
  });
199
315
  this.cleanupTempFiles();
200
316
  if (options.agentName && result.sessionId) {
317
+ logger.info({ agentName: options.agentName, sessionId: result.sessionId }, '[RUN_AGENT] Saving completed session ID.');
201
318
  await saveSessionId(options.agentName, result.sessionId, options.configPath, 'hermes');
202
319
  }
203
320
  return result;
204
321
  }
205
322
  catch (error) {
206
323
  this.cleanupTempFiles();
207
- logger.error({ error: error instanceof Error ? error.message : String(error), agentName: options.agentName }, 'Hermes runner failed');
324
+ logger.error({ error: error instanceof Error ? error.message : String(error), agentName: options.agentName }, '[RUN_AGENT] Hermes runner execution flow threw an error.');
208
325
  throw error;
209
326
  }
210
327
  }
@@ -213,29 +330,60 @@ export class NousHermesRunner {
213
330
  let { sessionId } = options;
214
331
  const cwd = options.cwd || process.cwd();
215
332
  const configPath = options.configPath || getWorkspaceDir();
333
+ logger.info({ agentName, autoResume, cwd, configPath, silent }, '[RUN_AGENT_INTERNAL] Starting internal agent runner workflow.');
216
334
  // Load .env files FIRST
217
- loadEnvQuietly(path.join(cwd, '.env'));
218
- loadEnvQuietly(path.join(cwd, '../Workflow/.env'));
335
+ const envPaths = [path.join(cwd, '.env'), path.join(cwd, '../Workflow/.env')];
336
+ for (const envPath of envPaths) {
337
+ if (fs.existsSync(envPath)) {
338
+ logger.debug({ envPath }, '[RUN_AGENT_INTERNAL] Loading quiet environment file.');
339
+ loadEnvQuietly(envPath);
340
+ }
341
+ else {
342
+ logger.debug({ envPath }, '[RUN_AGENT_INTERNAL] Environment file not found, skipping.');
343
+ }
344
+ }
219
345
  // Auto Resume
220
346
  if (autoResume && agentName && !sessionId) {
347
+ logger.info({ agentName }, '[RUN_AGENT_INTERNAL] Auto-resume enabled. Querying last session ID.');
221
348
  const lastId = await getLastSessionId(agentName, configPath, 'hermes');
222
349
  if (lastId) {
223
350
  sessionId = lastId;
224
351
  if (!silent)
225
352
  console.error(`[NousHermesRunner] Auto-resume session: ${sessionId}`);
353
+ logger.info({ sessionId }, '[RUN_AGENT_INTERNAL] Resolved last session ID for resume.');
354
+ }
355
+ else {
356
+ logger.info('[RUN_AGENT_INTERNAL] No previous session ID found for auto-resume.');
226
357
  }
227
358
  }
228
359
  const MAX_BUF = 10 * 1024 * 1024;
229
360
  const timeoutMs = this.timeoutMs;
230
361
  const HARD_TIMEOUT_MS = 60000;
231
- // HERMES_HOME setup — use the canonical helper (multi-OS, multi-install safe).
232
- // This replaces the previous cwd-relative resolution that caused HERMES_HOME
233
- // drift between dev/prod installs and between different spawn cwd's.
362
+ // ═══════════════════════════════════════════════════════════════════════════
363
+ // HERMES_HOME résolu via getAgentHermesHome() (multi-OS, multi-install).
364
+ // Priorité : OVERMIND_HERMES_HOME > <workspace>/.overmind/hermes/ > ~/.overmind/hermes/
365
+ // ═══════════════════════════════════════════════════════════════════════════
234
366
  const overmindHermesPath = getAgentOvermindHome(agentName);
235
367
  const overmindHermesSubPath = getAgentHermesHome(agentName);
236
- const agentSettingsPath = agentName ? resolveConfigPath(path.join(path.dirname(CONFIG.CLAUDE.PATHS.SETTINGS), `settings_${agentName}.json`), configPath) : '';
237
- if (agentName && !fs.existsSync(overmindHermesPath) && !fs.existsSync(agentSettingsPath)) {
238
- return { result: '', error: `INVALID_AGENT: Agent Hermes "${agentName}" non trouvé (HERMES_HOME=${overmindHermesSubPath}).` };
368
+ // ═══════════════════════════════════════════════════════════════════════════
369
+ // CHEMIN CREDENTIALS HERMES SOURCE DE VÉRITÉ UNIQUE
370
+ //
371
+ // <HERMES_HOME>/agents/<name>/settings.json
372
+ //
373
+ // ❌ NE PAS utiliser .claude/settings_<name>.json — c'est pour Claude/Kilo.
374
+ // ❌ Ce fichier EST le fichier natif Hermes. Le runner le lit ET le met à jour.
375
+ // ═══════════════════════════════════════════════════════════════════════════
376
+ const agentSettingsPath = agentName ? path.join(overmindHermesSubPath, 'settings.json') : '';
377
+ if (agentName && !fs.existsSync(overmindHermesSubPath)) {
378
+ return {
379
+ result: '',
380
+ error: `INVALID_AGENT: Dossier Hermes manquant pour l'agent "${agentName}". ` +
381
+ `Créez le dossier et le fichier : ${agentSettingsPath} ` +
382
+ `avec { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ` +
383
+ `"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ` +
384
+ `"ANTHROPIC_MODEL": "MiniMax-M3", "ANTHROPIC_PROVIDER": "minimax-cn" }, ` +
385
+ `"agent": "${agentName}", "runner": "hermes" }`,
386
+ };
239
387
  }
240
388
  // Load agent settings + MCP config (same pattern as ClaudeRunner)
241
389
  let systemPrompt = '';
@@ -243,7 +391,7 @@ export class NousHermesRunner {
243
391
  let resolvedProvider;
244
392
  const agentCustomEnv = {
245
393
  ...process.env,
246
- PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', PYTHONUNBUFFERED: '1',
394
+ PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: '1',
247
395
  PYTHONLEGACYWINDOWSSTDIO: '1', TERM: 'emacs',
248
396
  PROMPT_TOOLKIT_NO_INTERACTIVE: '1', ANSICON: '1',
249
397
  OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY || '',
@@ -307,12 +455,12 @@ export class NousHermesRunner {
307
455
  const canonicalSoul = path.join(overmindHermesSubPath, 'SOUL.md');
308
456
  const legacySoul = path.join(getSharedHermesHome(), `agent_${agentName}`, '.hermes', 'SOUL.md');
309
457
  const claudeSoul = path.join(configPath, '.claude', 'agents', `${agentName}.md`);
310
- const agentPromptPath = fs.existsSync(canonicalSoul)
311
- ? canonicalSoul
312
- : fs.existsSync(legacySoul)
313
- ? legacySoul
314
- : fs.existsSync(claudeSoul)
315
- ? claudeSoul
458
+ const agentPromptPath = fs.existsSync(claudeSoul)
459
+ ? claudeSoul
460
+ : fs.existsSync(canonicalSoul)
461
+ ? canonicalSoul
462
+ : fs.existsSync(legacySoul)
463
+ ? legacySoul
316
464
  : null;
317
465
  if (agentPromptPath && fs.existsSync(agentPromptPath)) {
318
466
  systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
@@ -330,24 +478,29 @@ export class NousHermesRunner {
330
478
  }
331
479
  }
332
480
  }
333
- // Load environment variables from .claude/settings_<agentName>.json
481
+ // ─────────────────────────────────────────────────────────────────────────
482
+ // LECTURE CREDENTIALS HERMES — <HERMES_HOME>/agents/<name>/settings.json
483
+ //
484
+ // Ce chemin est résolu UNE SEULE FOIS en haut de la fonction (agentSettingsPath).
485
+ // Il pointe vers le dossier natif Hermes, PAS vers .claude/.
486
+ // Si le fichier est absent, l'agent tourne sans LLM (erreur claire dans les logs).
487
+ // ─────────────────────────────────────────────────────────────────────────
334
488
  try {
335
- const agentSettingsPath = resolveConfigPath(path.join(path.dirname(CONFIG.CLAUDE.PATHS.SETTINGS), `settings_${agentName}.json`), configPath);
336
- // Diagnostic: if settings_<agent>.json is missing at the expected path, log it
337
- // explicitly along with the alternative paths that DO exist. Without this log,
338
- // a user putting the file under .claude/agents/ (or just `agents/`) will see a
339
- // cryptic 401 ten minutes later with no breadcrumb back to the misplacement.
489
+ // agentSettingsPath = <HERMES_HOME>/agents/<name>/settings.json (défini ligne ~405)
490
+ // NE PAS changer ceci pour pointer vers .claude/ c'est intentionnel.
340
491
  if (!fs.existsSync(agentSettingsPath)) {
341
- const altPaths = [
342
- path.join(configPath, '.claude', 'agents', `settings_${agentName}.json`),
343
- path.join(configPath, `settings_${agentName}.json`),
344
- path.join(configPath, 'agents', `settings_${agentName}.json`),
345
- ];
346
- logger.warn({
492
+ logger.error({
347
493
  agentName,
348
- searched: agentSettingsPath,
349
- alsoChecked: altPaths.filter((p) => fs.existsSync(p)),
350
- }, 'settings_<agent>.json not found at expected path. The agent will run with no LLM credentials unless ~/.hermes/.env or process.env provides them.');
494
+ expected: agentSettingsPath,
495
+ hermesHome: overmindHermesSubPath,
496
+ action: `Créer ${agentSettingsPath} avec les credentials. ` +
497
+ `Format minimal : { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ` +
498
+ `"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ` +
499
+ `"ANTHROPIC_MODEL": "MiniMax-M3", "ANTHROPIC_PROVIDER": "minimax-cn" }, ` +
500
+ `"agent": "${agentName}", "runner": "hermes" }`,
501
+ }, '[HERMES] ❌ settings.json introuvable dans le dossier Hermes natif. ' +
502
+ 'Hermes NE cherche PAS dans .claude/ — uniquement dans .overmind/hermes/agents/<name>/settings.json. ' +
503
+ 'Voir la documentation dans le commentaire du constructeur NousHermesRunner.');
351
504
  }
352
505
  if (fs.existsSync(agentSettingsPath)) {
353
506
  // Read the RAW settings (pre-interpolation) to capture $VAR references
@@ -366,8 +519,9 @@ export class NousHermesRunner {
366
519
  let settings = rawSettings;
367
520
  settings = interpolateEnvVars(settings);
368
521
  loadedSettings = settings;
369
- // Create temporary settings file
370
- const tempSettings = path.join(path.dirname(agentSettingsPath), `settings_${agentName}_tmp.json`);
522
+ // Fichier temporaire interpolé (valeurs $VAR résolues) — nettoyé après le spawn.
523
+ // Situé dans le même dossier que settings.json : <HERMES_HOME>/agents/<name>/
524
+ const tempSettings = path.join(path.dirname(agentSettingsPath), `settings_tmp.json`);
371
525
  fs.writeFileSync(tempSettings, JSON.stringify(settings, null, 2));
372
526
  tmpSettingsPath = tempSettings;
373
527
  this.tempFiles.push(tempSettings);
@@ -377,17 +531,22 @@ export class NousHermesRunner {
377
531
  agentCustomEnv.ANTHROPIC_MODEL = settings.env.MODEL;
378
532
  }
379
533
  }
380
- // MCP configurations
381
- const agentMcpPath = resolveConfigPath(path.join(path.dirname(CONFIG.CLAUDE.PATHS.SETTINGS), `.mcp.${agentName}.json`), configPath);
534
+ // ─────────────────────────────────────────────────────────────────────
535
+ // CONFIG MCP Résolution dans l'ordre suivant :
536
+ // 1. <HERMES_HOME>/agents/<name>/.mcp.json (override par agent)
537
+ // 2. <OVERMIND_WORKSPACE>/.mcp.json filtré par enabledMcpjsonServers
538
+ // ─────────────────────────────────────────────────────────────────────
539
+ const agentMcpPath = path.join(overmindHermesSubPath, '.mcp.json');
382
540
  if (fs.existsSync(agentMcpPath)) {
383
- // Write temporary mcp path
384
- const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_${agentName}_tmp.json`);
541
+ // Override MCP par agent : <HERMES_HOME>/agents/<name>/.mcp.json
542
+ const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_tmp.json`);
385
543
  fs.writeFileSync(tempMcp, fs.readFileSync(agentMcpPath, 'utf8'));
386
544
  tmpMcpPath = tempMcp;
387
545
  this.tempFiles.push(tempMcp);
388
546
  }
389
547
  else if (settings.enableAllProjectMcpServers === false &&
390
548
  Array.isArray(settings.enabledMcpjsonServers)) {
549
+ // Filtre le .mcp.json du workspace selon enabledMcpjsonServers
391
550
  const projectMcpPath = resolveConfigPath(CONFIG.CLAUDE.PATHS.MCP, configPath);
392
551
  if (fs.existsSync(projectMcpPath)) {
393
552
  const fullMcp = JSON.parse(fs.readFileSync(projectMcpPath, 'utf8'));
@@ -397,7 +556,7 @@ export class NousHermesRunner {
397
556
  filteredMcp.mcpServers[serverName] = fullMcp.mcpServers[serverName];
398
557
  }
399
558
  }
400
- const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_${agentName}_tmp.json`);
559
+ const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_tmp.json`);
401
560
  fs.writeFileSync(tempMcp, JSON.stringify(filteredMcp, null, 2));
402
561
  tmpMcpPath = tempMcp;
403
562
  this.tempFiles.push(tempMcp);
@@ -408,8 +567,8 @@ export class NousHermesRunner {
408
567
  catch (e) {
409
568
  logger.warn({ error: e }, `Failed to process settings/mcp configurations for Hermes agent ${agentName}`);
410
569
  }
411
- // Load environment from the agent's isolated .hermes/.env file, BUT only as a
412
- // fallback for keys that the explicit settings_<agent>.json did NOT set.
570
+ // Charge le .env de l'agent (dans <HERMES_HOME>/agents/<name>/.env) en FALLBACK UNIQUEMENT.
571
+ // Les clés déjà définies dans settings.json ne sont PAS écrasées.
413
572
  //
414
573
  // CRITICAL (2.8.29): The .hermes/.env file is a STALE WRITE of the previous
415
574
  // spawn — it gets re-written by the runner itself at line ~1013, but if a
@@ -468,7 +627,7 @@ export class NousHermesRunner {
468
627
  }
469
628
  const finalModel = options.model || resolvedModel || CONFIG.HERMES.DEFAULT_MODEL;
470
629
  const finalPrompt = systemPrompt ? `${systemPrompt}\n\n[USER QUERY]:\n${prompt}` : prompt;
471
- const cliPrompt = finalPrompt.length > 7000 ? finalPrompt.substring(0, 7000) : finalPrompt;
630
+ const cliPrompt = finalPrompt;
472
631
  // Build CLI args: chat -q (persistent session, NOT -z oneshot)
473
632
  // -z + --resume doesn't work — resume is ignored in oneshot mode
474
633
  //
@@ -493,25 +652,26 @@ export class NousHermesRunner {
493
652
  logger.info({ agentName, provider, model: finalModel }, '[HERMES_ARGS] Passing --provider (2.8.33: needed to bypass upstream auto-router that picked openrouter for MiniMax-M3).');
494
653
  }
495
654
  // ============================================================
496
- // 2.8.36 — PASS --toolsets TO ACTIVATE MCP SERVERS
655
+ // 2.8.36 — TOOLSET DISCOVERY (LOG ONLY NOT PASSED TO CLI)
497
656
  // ============================================================
498
- // Hermes upstream ONLY loads MCP servers that are explicitly listed via
499
- // -t/--toolsets (or registered in the agent's per-agent settings.json
500
- // under a `toolsets` key but the field is `enabledMcpjsonServers`,
501
- // which we already write). Looking at Hermes upstream's CLI: the
502
- // `enabledMcpjsonServers` field in settings.json is a *Hermes-side*
503
- // filter, but it requires the toolset to be discovered first via
504
- // `mcp.json` AND the runtime toolset registry to mark it enabled.
657
+ // Previously we passed MCP server names via `--toolsets` to Hermes.
658
+ // This caused `Warning: Unknown toolsets: mcp-memory, mcp-discord, ...`
659
+ // because Hermes's toolset registry does NOT use the `mcp-<name>` format.
660
+ //
661
+ // The MCP servers load correctly WITHOUT `--toolsets` because:
662
+ // 1. `HERMES_HOME` is overridden to the per-agent isolated run home.
663
+ // 2. `filterConfigYaml` writes a filtered `config.yaml` that lists only
664
+ // the allowed MCP servers under `mcp_servers:`.
665
+ // 3. Hermes reads `config.yaml` at startup and connects to every server
666
+ // listed there — no `--toolsets` flag needed.
505
667
  //
506
- // The simplest, most reliable cross-version trick: pass the MCP server
507
- // names as a comma-separated list via `--toolsets`. Hermes upstream
508
- // will then ensure those toolsets are loaded AND exposed to the
509
- // conversation as `mcp__<server>__<tool>` callable tools.
668
+ // We still build `toolsetList` for diagnostic logging so that the log
669
+ // shows which MCP servers are expected for this agent run.
510
670
  //
511
671
  // Source of truth for the list:
512
672
  // - If settings has `enabledMcpjsonServers: [...non-empty...]`, use that.
513
673
  // - Else, if `enableAllProjectMcpServers: true`, use ALL server names
514
- // from Workflow/.mcp.json.
674
+ // from the Hermes config.yaml.
515
675
  // - Else, skip — no toolset hint.
516
676
  const toolsetList = [];
517
677
  // Read the canonical settings.json we just wrote to find the MCP server hints.
@@ -570,19 +730,33 @@ export class NousHermesRunner {
570
730
  const hermesConfigPath = path.join(getSharedHermesHome(), 'config.yaml');
571
731
  if (fs.existsSync(hermesConfigPath)) {
572
732
  const yamlText = fs.readFileSync(hermesConfigPath, 'utf8');
573
- // Tiny ad-hoc YAML parser for `mcp_servers:` block: capture the
574
- // top-level keys under that section. This is intentionally
575
- // dependency-free we don't want to pull in a YAML lib for a
576
- // single field. Hermes' config.yaml uses simple `key: value` lines
577
- // and 2-space indent for the mcp_servers block.
578
- const mcpBlockMatch = yamlText.match(/^mcp_servers:\s*\n((?: {2}[^\n]+\n?)+)/m);
579
- if (mcpBlockMatch) {
580
- const block = mcpBlockMatch[1];
581
- const serverNames = [...block.matchAll(/^ {2}([a-zA-Z0-9_-]+):/gm)].map((m) => m[1]);
582
- if (serverNames.length > 0) {
583
- toolsetList.push(...serverNames);
733
+ // Line-by-line YAML parser for the `mcp_servers` section (handles comments, varying indentation, and exits correctly)
734
+ const lines = yamlText.split(/\r?\n/);
735
+ let inMcpServers = false;
736
+ const serverNames = [];
737
+ for (const line of lines) {
738
+ const trimmed = line.trim();
739
+ if (!trimmed || trimmed.startsWith('#'))
740
+ continue;
741
+ if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
742
+ inMcpServers = true;
743
+ continue;
744
+ }
745
+ if (inMcpServers) {
746
+ const indentMatch = line.match(/^(\s+)/);
747
+ if (!indentMatch) {
748
+ inMcpServers = false;
749
+ continue;
750
+ }
751
+ const keyMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*:/);
752
+ if (keyMatch) {
753
+ serverNames.push(keyMatch[1]);
754
+ }
584
755
  }
585
756
  }
757
+ if (serverNames.length > 0) {
758
+ toolsetList.push(...serverNames);
759
+ }
586
760
  }
587
761
  if (toolsetList.length === 0) {
588
762
  // Fallback: read .mcp.json (Overmind format) for any servers not in
@@ -601,8 +775,10 @@ export class NousHermesRunner {
601
775
  }
602
776
  }
603
777
  if (toolsetList.length > 0) {
604
- cleanArgs.push('--toolsets', toolsetList.join(','));
605
- logger.info({ agentName, toolsets: toolsetList }, '[HERMES_ARGS] Passing --toolsets (2.8.36: activates MCP servers as callable tools in this session).');
778
+ // Log which MCP servers are expected for this run — do NOT pass to CLI.
779
+ // Hermes loads them via the isolated config.yaml; passing --toolsets
780
+ // would produce `Warning: Unknown toolsets: mcp-<name>` noise with no benefit.
781
+ logger.info({ agentName, expectedMcpServers: toolsetList }, '[HERMES_ARGS] Expected MCP servers for this run (loaded via config.yaml, NOT via --toolsets).');
606
782
  }
607
783
  if (sessionId)
608
784
  cleanArgs.push('--resume', sessionId);
@@ -690,10 +866,10 @@ export class NousHermesRunner {
690
866
  `but process.env.${varName} is empty. Add it to /home/demon/.overmind/.env or fix the settings reference.`);
691
867
  }
692
868
  resolvedValue = fromEnv;
693
- logger.info({ agentName, sourceKey: t.key, referencedVar: varName, resolvedLen: resolvedValue.length }, '[SUBTILISATION] Resolved $VAR reference from settings_<agent>.json against process.env.');
869
+ logger.info({ agentName, sourceKey: t.key, referencedVar: varName, resolvedLen: resolvedValue.length }, '[TOKEN_RESOLVER] Resolved $VAR reference from settings_<agent>.json against process.env.');
694
870
  }
695
871
  const detected = detectTokenProvider(resolvedValue);
696
- logger.info({ agentName, tokenKey: t.key, detectedProvider: detected.provider, mappedTo: detected.envKey }, '[SUBTILISATION] Using explicit settings_<agent>.json token, re-mapping to detected provider env var.');
872
+ logger.info({ agentName, tokenKey: t.key, detectedProvider: detected.provider, mappedTo: detected.envKey }, '[TOKEN_RESOLVER] Using explicit settings_<agent>.json token, re-mapping to detected provider env var.');
697
873
  return { tokenEnvKey: t.key, tokenValue: resolvedValue, detectedProvider: detected.provider, source: 'settings-explicit' };
698
874
  }
699
875
  const candidates = [];
@@ -722,7 +898,7 @@ export class NousHermesRunner {
722
898
  // Pass B: re-map the first candidate to the right provider env-var name.
723
899
  const first = candidates[0];
724
900
  if (first.detected.provider !== 'unknown' && first.detected.envKey !== first.key) {
725
- logger.info({ agentName, sourceKey: first.key, detectedProvider: first.detected.provider, remappedTo: first.detected.envKey }, '[SUBTILISATION] Token prefix detected provider mismatch — re-mapping env var.');
901
+ logger.info({ agentName, sourceKey: first.key, detectedProvider: first.detected.provider, remappedTo: first.detected.envKey }, '[TOKEN_RESOLVER] Token prefix detected provider mismatch — re-mapping env var.');
726
902
  return {
727
903
  tokenEnvKey: first.detected.envKey,
728
904
  tokenValue: first.value,
@@ -842,25 +1018,27 @@ export class NousHermesRunner {
842
1018
  // AbortSignal
843
1019
  if (options.signal?.aborted)
844
1020
  return Promise.reject(new Error('ABORTED'));
845
- // =============================================================
846
- // 2.8.30 WRITE CANONICAL Hermes SETTINGS
847
- // =============================================================
848
- // Hermes upstream uses the standard appdirs-style layout:
849
- // <HERMES_HOME>/agents/<name>/settings.json ← per-agent env, mcp, persona
850
- // <HERMES_HOME>/agents/<name>/SOUL.md ← per-agent system prompt
851
- // <HERMES_HOME>/config.yaml ← global, Hermes manages
852
- // <HERMES_HOME>/auth.json ← global, Hermes manages
1021
+ // ═══════════════════════════════════════════════════════════════════════════
1022
+ // MISE À JOUR settings.json Hermes natif (2.8.45)
1023
+ // ═══════════════════════════════════════════════════════════════════════════
1024
+ //
1025
+ // Le runner LIT et ÉCRIT dans le MÊME fichier :
1026
+ // <HERMES_HOME>/agents/<name>/settings.json
853
1027
  //
854
- // Overmind's only job here is to:
855
- // 1. Convert Workflow/.claude/settings_<name>.json (Overmind runner format)
856
- // into the canonical Hermes format and write it to <HERMES_HOME>/agents/<name>/settings.json
857
- // 2. Make sure <HERMES_HOME>/agents/<name>/SOUL.md exists (canonical path)
858
- // 3. Let Hermes upstream manage config.yaml, auth.json, sessions/, etc.
1028
+ // Cycle complet :
1029
+ // 1. LECTURE : settings.json interpolation $VAR → agentCustomEnv
1030
+ // 2. DÉTECTION: token prefix (sk-cp-* MiniMax, 32hex Z.AI, etc.)
1031
+ // 3. INJECTION: MINIMAX_CN_API_KEY, MINIMAX_CN_BASE_URL, etc. auto-injectés
1032
+ // 4. ÉCRITURE : settings.json mis à jour avec les clés injectées
1033
+ // 5. SPAWN : hermes chat -q avec HERMES_HOME = runs/<name>/
859
1034
  //
860
- // This replaces the previous "polylgot" hack where Overmind wrote
861
- // `<HERMES_HOME>/agent_<name>/.hermes/.env`, `.hermes/config.yaml`, and
862
- // `.hermes/auth.json` files that don't match Hermes's expected layout
863
- // and caused credential drift + silent 401s.
1035
+ // Overmind NE convertit PAS depuis .claude/ c'est le chemin Claude/Kilo.
1036
+ // settings.json est géré par l'utilisateur ET mis à jour par Overmind.
1037
+ // Les $VAR restent en tant que littéraux après la première résolution.
1038
+ // Pour changer un credential, éditer directement settings.json.
1039
+ //
1040
+ // Autres fichiers gérés par Hermes upstream (ne pas toucher) :
1041
+ // config.yaml, auth.json, sessions/, logs/
864
1042
  if (agentName) {
865
1043
  const agentHome = overmindHermesSubPath; // = <HERMES_HOME>/agents/<name>/
866
1044
  if (!fs.existsSync(agentHome))
@@ -963,6 +1141,73 @@ export class NousHermesRunner {
963
1141
  // edits were being silently deleted after each spawn.
964
1142
  logger.info({ agentName, settingsPath: tmpAgentSettings, envKeys: Object.keys(settingsJson.env || {}).length }, '[HERMES] Wrote canonical agents/<name>/settings.json (env block from settings_<name>.json + provider-specific seeds).');
965
1143
  }
1144
+ let effectiveHermesHome = sharedHome;
1145
+ if (agentName && enabledInSettings.length > 0) {
1146
+ try {
1147
+ const runsDir = path.join(sharedHome, 'runs');
1148
+ if (!fs.existsSync(runsDir))
1149
+ fs.mkdirSync(runsDir, { recursive: true });
1150
+ const runHome = path.join(runsDir, agentName);
1151
+ if (!fs.existsSync(runHome))
1152
+ fs.mkdirSync(runHome, { recursive: true });
1153
+ // Copy auth.json if exists
1154
+ const sharedAuth = path.join(sharedHome, 'auth.json');
1155
+ const runAuth = path.join(runHome, 'auth.json');
1156
+ if (fs.existsSync(sharedAuth)) {
1157
+ fs.copyFileSync(sharedAuth, runAuth);
1158
+ }
1159
+ // Robust directory junction/symlink helper
1160
+ const linkDirRobust = (target, source) => {
1161
+ if (!fs.existsSync(source)) {
1162
+ try {
1163
+ fs.mkdirSync(source, { recursive: true });
1164
+ }
1165
+ catch (e) {
1166
+ logger.warn({ source, error: e }, '[HERMES_HOME] Failed to create link source directory');
1167
+ }
1168
+ }
1169
+ let exists = false;
1170
+ let stats = null;
1171
+ try {
1172
+ stats = fs.lstatSync(target);
1173
+ exists = true;
1174
+ }
1175
+ catch {
1176
+ // target does not exist
1177
+ }
1178
+ if (exists) {
1179
+ logger.debug({ target, isJunction: stats ? (stats.isSymbolicLink() || stats.isDirectory()) : false }, '[HERMES_HOME] Target directory/link already exists. Skipping link creation.');
1180
+ }
1181
+ else {
1182
+ logger.info({ target, source }, '[HERMES_HOME] Creating junction/symbolic link.');
1183
+ if (process.platform === 'win32') {
1184
+ execSync(`cmd /c mklink /J "${target}" "${source}"`);
1185
+ }
1186
+ else {
1187
+ fs.symlinkSync(source, target);
1188
+ }
1189
+ }
1190
+ };
1191
+ // Link agents directory
1192
+ const sharedAgents = path.join(sharedHome, 'agents');
1193
+ const runAgents = path.join(runHome, 'agents');
1194
+ linkDirRobust(runAgents, sharedAgents);
1195
+ // Link sessions directory
1196
+ const sharedSessions = path.join(sharedHome, 'sessions');
1197
+ const runSessions = path.join(runHome, 'sessions');
1198
+ linkDirRobust(runSessions, sharedSessions);
1199
+ // Generate filtered config.yaml
1200
+ const sharedConfig = path.join(sharedHome, 'config.yaml');
1201
+ const runConfig = path.join(runHome, 'config.yaml');
1202
+ const filteredConfig = filterConfigYaml(sharedConfig, enabledInSettings);
1203
+ fs.writeFileSync(runConfig, filteredConfig, 'utf8');
1204
+ effectiveHermesHome = runHome;
1205
+ logger.info({ agentName, runHome, enabledMcp: enabledInSettings }, '[HERMES_HOME] Isolated agent Hermes Home and filtered config.yaml successfully.');
1206
+ }
1207
+ catch (e) {
1208
+ logger.warn({ error: e }, 'Failed to setup isolated run home; falling back to sharedHome');
1209
+ }
1210
+ }
966
1211
  // AbortSignal
967
1212
  if (options.signal?.aborted)
968
1213
  return Promise.reject(new Error('ABORTED'));
@@ -972,10 +1217,27 @@ export class NousHermesRunner {
972
1217
  let retryCount = 0;
973
1218
  const maxRetries = getAvailableFallbacks().length + 1;
974
1219
  let currentSessionId = sessionId;
975
- const safeResolve = (v) => { if (!resolved) {
976
- resolved = true;
977
- resolve(v);
978
- } };
1220
+ const abortListener = () => {
1221
+ if (currentChildRef) {
1222
+ killProcessTree(currentChildRef).then(() => {
1223
+ cleanupTmpFiles();
1224
+ safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
1225
+ });
1226
+ }
1227
+ else {
1228
+ cleanupTmpFiles();
1229
+ safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
1230
+ }
1231
+ };
1232
+ const safeResolve = (v) => {
1233
+ if (!resolved) {
1234
+ resolved = true;
1235
+ if (options.signal) {
1236
+ options.signal.removeEventListener('abort', abortListener);
1237
+ }
1238
+ resolve(v);
1239
+ }
1240
+ };
979
1241
  const cleanupTmpFiles = () => {
980
1242
  for (const f of [tmpSettingsPath, tmpMcpPath]) {
981
1243
  if (f && fs.existsSync(f)) {
@@ -1064,12 +1326,11 @@ export class NousHermesRunner {
1064
1326
  // etc. relative to HERMES_HOME. Setting it to the per-agent home would
1065
1327
  // tell Hermes "this IS the Hermes root" and make it look for config.yaml
1066
1328
  // IN the agent dir — wrong layout.
1067
- const sharedHermesHome = getSharedHermesHome();
1068
1329
  const child = spawn(hermesBin, cleanArgs, {
1069
1330
  cwd, shell: false, windowsHide: true,
1070
1331
  env: {
1071
1332
  ...spawnEnv,
1072
- HERMES_HOME: sharedHermesHome,
1333
+ HERMES_HOME: effectiveHermesHome,
1073
1334
  ...(isVenvInstall
1074
1335
  ? {
1075
1336
  VIRTUAL_ENV: venvRoot,
@@ -1100,8 +1361,8 @@ export class NousHermesRunner {
1100
1361
  void appendOutput(child.pid, chunk, configPath);
1101
1362
  void appendLiveOutput(child.pid, chunk);
1102
1363
  }
1103
- if (stdout.length + chunk.length > MAX_BUF)
1104
- stdout = stdout.slice(-MAX_BUF);
1364
+ if (stdout.length + chunk.length > this.MAX_BUF)
1365
+ stdout = stdout.slice(-this.MAX_BUF);
1105
1366
  else
1106
1367
  stdout += chunk;
1107
1368
  if (!silent && agentName)
@@ -1109,8 +1370,8 @@ export class NousHermesRunner {
1109
1370
  });
1110
1371
  child.stderr?.on('data', (d) => {
1111
1372
  const chunk = d.toString();
1112
- if (stderr.length + chunk.length > MAX_BUF)
1113
- stderr = stderr.slice(-MAX_BUF);
1373
+ if (stderr.length + chunk.length > this.MAX_BUF)
1374
+ stderr = stderr.slice(-this.MAX_BUF);
1114
1375
  else
1115
1376
  stderr += chunk;
1116
1377
  if (!silent && agentName)
@@ -1170,13 +1431,9 @@ export class NousHermesRunner {
1170
1431
  });
1171
1432
  });
1172
1433
  };
1173
- options.signal?.addEventListener('abort', () => {
1174
- if (currentChildRef)
1175
- killProcessTree(currentChildRef).then(() => {
1176
- cleanupTmpFiles();
1177
- safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
1178
- });
1179
- });
1434
+ if (options.signal) {
1435
+ options.signal.addEventListener('abort', abortListener);
1436
+ }
1180
1437
  let firstToken;
1181
1438
  try {
1182
1439
  firstToken = getTokenForIndex(0);