overmind-mcp 2.8.40 → 2.8.44

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,12 +172,12 @@ 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 polyglotte pour Hermes Agent (Overmind 2.8.27+).
152
176
  *
153
177
  * • Providers : OpenAI, MiniMax GLOBAL/CN, Zhipu/GLM, Mistral, NVIDIA NIM, OpenRouter (embeddings only)
154
178
  * • Lit settings_<agent>.json + .mcp.<agent>.json depuis .claude/ comme les autres runners
155
179
  * • 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,
180
+ * • Sélection/Emprunt de jeton 3-pass (Pass 1: settings-explicit, Pass A: prefer provider-specific,
157
181
  * Pass B: re-map generic key, Pass C: rare fallback) — see hermesTokenResolver.ts
158
182
  * • CN/GLOBAL disambiguation for sk-cp-* via ANTHROPIC_BASE_URL (URL wins)
159
183
  * • OVERMIND_MINIMAX_DEFAULT=cn|global|auto for setups where all MiniMax tokens are CN
@@ -163,6 +187,62 @@ async function findHermesBinary() {
163
187
  * to prevent Hermes from picking an exhausted bucket from a previous provider config
164
188
  * • HOME/USERPROFILE propagated to spawned Hermes so ~/.hermes lookups resolve canonically
165
189
  */
190
+ function filterConfigYaml(sourceYamlPath, allowedServers) {
191
+ try {
192
+ if (!fs.existsSync(sourceYamlPath))
193
+ return 'mcp_servers: {}\n';
194
+ const yamlText = fs.readFileSync(sourceYamlPath, 'utf8');
195
+ const lines = yamlText.split(/\r?\n/);
196
+ let inMcpServers = false;
197
+ let currentServerName = '';
198
+ const serverBlocks = {};
199
+ const headerLines = [];
200
+ for (const line of lines) {
201
+ const trimmed = line.trim();
202
+ if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
203
+ inMcpServers = true;
204
+ headerLines.push(line);
205
+ continue;
206
+ }
207
+ if (inMcpServers) {
208
+ const indentMatch = line.match(/^(\s+)/);
209
+ if (!indentMatch) {
210
+ inMcpServers = false;
211
+ headerLines.push(line);
212
+ continue;
213
+ }
214
+ const indent = indentMatch[1].length;
215
+ const keyMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*:/);
216
+ if (keyMatch && indent === 2) {
217
+ currentServerName = keyMatch[1];
218
+ serverBlocks[currentServerName] = [line];
219
+ }
220
+ else if (currentServerName) {
221
+ serverBlocks[currentServerName].push(line);
222
+ }
223
+ }
224
+ else {
225
+ headerLines.push(line);
226
+ }
227
+ }
228
+ let result = '';
229
+ for (const line of headerLines) {
230
+ result += line + '\n';
231
+ if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
232
+ for (const srv of allowedServers) {
233
+ if (serverBlocks[srv]) {
234
+ result += serverBlocks[srv].join('\n') + '\n';
235
+ }
236
+ }
237
+ }
238
+ }
239
+ return result;
240
+ }
241
+ catch (err) {
242
+ logger.error({ sourceYamlPath, error: err }, '[YAML_FILTER] Unexpected failure while filtering config.yaml.');
243
+ return 'mcp_servers: {}\n';
244
+ }
245
+ }
166
246
  export class NousHermesRunner {
167
247
  timeoutMs;
168
248
  tempFiles = [];
@@ -171,20 +251,22 @@ export class NousHermesRunner {
171
251
  this.timeoutMs = CONFIG.TIMEOUT_MS || 900000; // 15 min default
172
252
  }
173
253
  cleanupTempFiles() {
254
+ logger.debug({ count: this.tempFiles.length }, '[CLEANUP] Cleaning up temporary run files.');
174
255
  for (const tempFile of this.tempFiles) {
175
256
  try {
176
257
  if (fs.existsSync(tempFile)) {
177
258
  fs.unlinkSync(tempFile);
178
- logger.debug({ tempFile }, 'Cleaned up temp file');
259
+ logger.debug({ tempFile }, '[CLEANUP] Cleaned up temp file');
179
260
  }
180
261
  }
181
262
  catch (err) {
182
- logger.warn({ tempFile, error: err }, 'Failed to cleanup temp file');
263
+ logger.warn({ tempFile, error: err }, '[CLEANUP] Failed to cleanup temp file');
183
264
  }
184
265
  }
185
266
  this.tempFiles = [];
186
267
  }
187
268
  async runAgent(options) {
269
+ logger.info({ agentName: options.agentName, model: options.model, sessionId: options.sessionId }, '[RUN_AGENT] Initiating runAgent entrypoint.');
188
270
  try {
189
271
  const result = await withSpan('hermes.runAgent', async (span) => {
190
272
  span.setAttribute('agentName', options.agentName || '');
@@ -198,13 +280,14 @@ export class NousHermesRunner {
198
280
  });
199
281
  this.cleanupTempFiles();
200
282
  if (options.agentName && result.sessionId) {
283
+ logger.info({ agentName: options.agentName, sessionId: result.sessionId }, '[RUN_AGENT] Saving completed session ID.');
201
284
  await saveSessionId(options.agentName, result.sessionId, options.configPath, 'hermes');
202
285
  }
203
286
  return result;
204
287
  }
205
288
  catch (error) {
206
289
  this.cleanupTempFiles();
207
- logger.error({ error: error instanceof Error ? error.message : String(error), agentName: options.agentName }, 'Hermes runner failed');
290
+ logger.error({ error: error instanceof Error ? error.message : String(error), agentName: options.agentName }, '[RUN_AGENT] Hermes runner execution flow threw an error.');
208
291
  throw error;
209
292
  }
210
293
  }
@@ -213,16 +296,30 @@ export class NousHermesRunner {
213
296
  let { sessionId } = options;
214
297
  const cwd = options.cwd || process.cwd();
215
298
  const configPath = options.configPath || getWorkspaceDir();
299
+ logger.info({ agentName, autoResume, cwd, configPath, silent }, '[RUN_AGENT_INTERNAL] Starting internal agent runner workflow.');
216
300
  // Load .env files FIRST
217
- loadEnvQuietly(path.join(cwd, '.env'));
218
- loadEnvQuietly(path.join(cwd, '../Workflow/.env'));
301
+ const envPaths = [path.join(cwd, '.env'), path.join(cwd, '../Workflow/.env')];
302
+ for (const envPath of envPaths) {
303
+ if (fs.existsSync(envPath)) {
304
+ logger.debug({ envPath }, '[RUN_AGENT_INTERNAL] Loading quiet environment file.');
305
+ loadEnvQuietly(envPath);
306
+ }
307
+ else {
308
+ logger.debug({ envPath }, '[RUN_AGENT_INTERNAL] Environment file not found, skipping.');
309
+ }
310
+ }
219
311
  // Auto Resume
220
312
  if (autoResume && agentName && !sessionId) {
313
+ logger.info({ agentName }, '[RUN_AGENT_INTERNAL] Auto-resume enabled. Querying last session ID.');
221
314
  const lastId = await getLastSessionId(agentName, configPath, 'hermes');
222
315
  if (lastId) {
223
316
  sessionId = lastId;
224
317
  if (!silent)
225
318
  console.error(`[NousHermesRunner] Auto-resume session: ${sessionId}`);
319
+ logger.info({ sessionId }, '[RUN_AGENT_INTERNAL] Resolved last session ID for resume.');
320
+ }
321
+ else {
322
+ logger.info('[RUN_AGENT_INTERNAL] No previous session ID found for auto-resume.');
226
323
  }
227
324
  }
228
325
  const MAX_BUF = 10 * 1024 * 1024;
@@ -233,7 +330,8 @@ export class NousHermesRunner {
233
330
  // drift between dev/prod installs and between different spawn cwd's.
234
331
  const overmindHermesPath = getAgentOvermindHome(agentName);
235
332
  const overmindHermesSubPath = getAgentHermesHome(agentName);
236
- if (agentName && !fs.existsSync(overmindHermesPath)) {
333
+ const agentSettingsPath = agentName ? resolveConfigPath(path.join(path.dirname(CONFIG.CLAUDE.PATHS.SETTINGS), `settings_${agentName}.json`), configPath) : '';
334
+ if (agentName && !fs.existsSync(overmindHermesPath) && !fs.existsSync(agentSettingsPath)) {
237
335
  return { result: '', error: `INVALID_AGENT: Agent Hermes "${agentName}" non trouvé (HERMES_HOME=${overmindHermesSubPath}).` };
238
336
  }
239
337
  // Load agent settings + MCP config (same pattern as ClaudeRunner)
@@ -242,7 +340,7 @@ export class NousHermesRunner {
242
340
  let resolvedProvider;
243
341
  const agentCustomEnv = {
244
342
  ...process.env,
245
- PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', PYTHONUNBUFFERED: '1',
343
+ PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: '1',
246
344
  PYTHONLEGACYWINDOWSSTDIO: '1', TERM: 'emacs',
247
345
  PROMPT_TOOLKIT_NO_INTERACTIVE: '1', ANSICON: '1',
248
346
  OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY || '',
@@ -261,6 +359,7 @@ export class NousHermesRunner {
261
359
  };
262
360
  let tmpSettingsPath = null;
263
361
  let tmpMcpPath = null;
362
+ let loadedSettings = null;
264
363
  // Capture the RAW (pre-interpolation) settings tokens so getTokenForIndex can
265
364
  // fail-loud on unresolved $VAR references and report which one is missing.
266
365
  // (Once interpolateEnvVars() runs, $VAR has been replaced with its value, and
@@ -304,9 +403,29 @@ export class NousHermesRunner {
304
403
  // agents that haven't been migrated yet.
305
404
  const canonicalSoul = path.join(overmindHermesSubPath, 'SOUL.md');
306
405
  const legacySoul = path.join(getSharedHermesHome(), `agent_${agentName}`, '.hermes', 'SOUL.md');
307
- const agentPromptPath = fs.existsSync(canonicalSoul) ? canonicalSoul : legacySoul;
308
- if (fs.existsSync(agentPromptPath)) {
406
+ const claudeSoul = path.join(configPath, '.claude', 'agents', `${agentName}.md`);
407
+ const agentPromptPath = fs.existsSync(claudeSoul)
408
+ ? claudeSoul
409
+ : fs.existsSync(canonicalSoul)
410
+ ? canonicalSoul
411
+ : fs.existsSync(legacySoul)
412
+ ? legacySoul
413
+ : null;
414
+ if (agentPromptPath && fs.existsSync(agentPromptPath)) {
309
415
  systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
416
+ // Sync system prompt to canonical layout if loaded from legacy or Claude paths
417
+ if (agentPromptPath !== canonicalSoul) {
418
+ try {
419
+ if (!fs.existsSync(overmindHermesSubPath)) {
420
+ fs.mkdirSync(overmindHermesSubPath, { recursive: true });
421
+ }
422
+ fs.writeFileSync(canonicalSoul, systemPrompt, 'utf8');
423
+ logger.info({ agentName, source: agentPromptPath, target: canonicalSoul }, 'Synced SOUL.md to canonical Hermes path.');
424
+ }
425
+ catch (e) {
426
+ logger.warn({ error: e }, 'Failed to sync SOUL.md to canonical Hermes path');
427
+ }
428
+ }
310
429
  }
311
430
  // Load environment variables from .claude/settings_<agentName>.json
312
431
  try {
@@ -343,6 +462,7 @@ export class NousHermesRunner {
343
462
  }
344
463
  let settings = rawSettings;
345
464
  settings = interpolateEnvVars(settings);
465
+ loadedSettings = settings;
346
466
  // Create temporary settings file
347
467
  const tempSettings = path.join(path.dirname(agentSettingsPath), `settings_${agentName}_tmp.json`);
348
468
  fs.writeFileSync(tempSettings, JSON.stringify(settings, null, 2));
@@ -445,7 +565,7 @@ export class NousHermesRunner {
445
565
  }
446
566
  const finalModel = options.model || resolvedModel || CONFIG.HERMES.DEFAULT_MODEL;
447
567
  const finalPrompt = systemPrompt ? `${systemPrompt}\n\n[USER QUERY]:\n${prompt}` : prompt;
448
- const cliPrompt = finalPrompt.length > 7000 ? finalPrompt.substring(0, 7000) : finalPrompt;
568
+ const cliPrompt = finalPrompt;
449
569
  // Build CLI args: chat -q (persistent session, NOT -z oneshot)
450
570
  // -z + --resume doesn't work — resume is ignored in oneshot mode
451
571
  //
@@ -497,19 +617,29 @@ export class NousHermesRunner {
497
617
  const effectiveSettings = {};
498
618
  if (agentName) {
499
619
  try {
500
- const canonicalPath = path.join(overmindHermesSubPath, 'settings.json');
501
- if (fs.existsSync(canonicalPath)) {
502
- const raw = JSON.parse(fs.readFileSync(canonicalPath, 'utf8'));
503
- if (Array.isArray(raw.enabledMcpjsonServers)) {
504
- effectiveSettings.enabledMcpjsonServers = raw.enabledMcpjsonServers.filter(Boolean);
620
+ if (loadedSettings) {
621
+ if (Array.isArray(loadedSettings.enabledMcpjsonServers)) {
622
+ effectiveSettings.enabledMcpjsonServers = loadedSettings.enabledMcpjsonServers.filter(Boolean);
505
623
  }
506
- if (raw.enableAllProjectMcpServers !== undefined) {
507
- effectiveSettings.enableAllProjectMcpServers = raw.enableAllProjectMcpServers === true;
624
+ if (loadedSettings.enableAllProjectMcpServers !== undefined) {
625
+ effectiveSettings.enableAllProjectMcpServers = loadedSettings.enableAllProjectMcpServers === true;
626
+ }
627
+ }
628
+ else {
629
+ const canonicalPath = path.join(overmindHermesSubPath, 'settings.json');
630
+ if (fs.existsSync(canonicalPath)) {
631
+ const raw = JSON.parse(fs.readFileSync(canonicalPath, 'utf8'));
632
+ if (Array.isArray(raw.enabledMcpjsonServers)) {
633
+ effectiveSettings.enabledMcpjsonServers = raw.enabledMcpjsonServers.filter(Boolean);
634
+ }
635
+ if (raw.enableAllProjectMcpServers !== undefined) {
636
+ effectiveSettings.enableAllProjectMcpServers = raw.enableAllProjectMcpServers === true;
637
+ }
508
638
  }
509
639
  }
510
640
  }
511
641
  catch (e) {
512
- logger.warn({ error: e }, '[HERMES_ARGS] Failed to read canonical settings.json for toolset hints.');
642
+ logger.warn({ error: e }, '[HERMES_ARGS] Failed to read settings for toolset hints.');
513
643
  }
514
644
  }
515
645
  const enabledInSettings = effectiveSettings.enabledMcpjsonServers || [];
@@ -537,19 +667,33 @@ export class NousHermesRunner {
537
667
  const hermesConfigPath = path.join(getSharedHermesHome(), 'config.yaml');
538
668
  if (fs.existsSync(hermesConfigPath)) {
539
669
  const yamlText = fs.readFileSync(hermesConfigPath, 'utf8');
540
- // Tiny ad-hoc YAML parser for `mcp_servers:` block: capture the
541
- // top-level keys under that section. This is intentionally
542
- // dependency-free we don't want to pull in a YAML lib for a
543
- // single field. Hermes' config.yaml uses simple `key: value` lines
544
- // and 2-space indent for the mcp_servers block.
545
- const mcpBlockMatch = yamlText.match(/^mcp_servers:\s*\n((?: {2}[^\n]+\n?)+)/m);
546
- if (mcpBlockMatch) {
547
- const block = mcpBlockMatch[1];
548
- const serverNames = [...block.matchAll(/^ {2}([a-zA-Z0-9_-]+):/gm)].map((m) => m[1]);
549
- if (serverNames.length > 0) {
550
- toolsetList.push(...serverNames);
670
+ // Line-by-line YAML parser for the `mcp_servers` section (handles comments, varying indentation, and exits correctly)
671
+ const lines = yamlText.split(/\r?\n/);
672
+ let inMcpServers = false;
673
+ const serverNames = [];
674
+ for (const line of lines) {
675
+ const trimmed = line.trim();
676
+ if (!trimmed || trimmed.startsWith('#'))
677
+ continue;
678
+ if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
679
+ inMcpServers = true;
680
+ continue;
681
+ }
682
+ if (inMcpServers) {
683
+ const indentMatch = line.match(/^(\s+)/);
684
+ if (!indentMatch) {
685
+ inMcpServers = false;
686
+ continue;
687
+ }
688
+ const keyMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*:/);
689
+ if (keyMatch) {
690
+ serverNames.push(keyMatch[1]);
691
+ }
551
692
  }
552
693
  }
694
+ if (serverNames.length > 0) {
695
+ toolsetList.push(...serverNames);
696
+ }
553
697
  }
554
698
  if (toolsetList.length === 0) {
555
699
  // Fallback: read .mcp.json (Overmind format) for any servers not in
@@ -568,8 +712,9 @@ export class NousHermesRunner {
568
712
  }
569
713
  }
570
714
  if (toolsetList.length > 0) {
571
- cleanArgs.push('--toolsets', toolsetList.join(','));
572
- logger.info({ agentName, toolsets: toolsetList }, '[HERMES_ARGS] Passing --toolsets (2.8.36: activates MCP servers as callable tools in this session).');
715
+ const formattedToolsets = toolsetList.map((name) => name.startsWith('mcp-') ? name : `mcp-${name}`);
716
+ cleanArgs.push('--toolsets', formattedToolsets.join(','));
717
+ logger.info({ agentName, toolsets: formattedToolsets }, '[HERMES_ARGS] Passing --toolsets (2.8.36: activates MCP servers as callable tools in this session).');
573
718
  }
574
719
  if (sessionId)
575
720
  cleanArgs.push('--resume', sessionId);
@@ -657,10 +802,10 @@ export class NousHermesRunner {
657
802
  `but process.env.${varName} is empty. Add it to /home/demon/.overmind/.env or fix the settings reference.`);
658
803
  }
659
804
  resolvedValue = fromEnv;
660
- logger.info({ agentName, sourceKey: t.key, referencedVar: varName, resolvedLen: resolvedValue.length }, '[SUBTILISATION] Resolved $VAR reference from settings_<agent>.json against process.env.');
805
+ logger.info({ agentName, sourceKey: t.key, referencedVar: varName, resolvedLen: resolvedValue.length }, '[TOKEN_RESOLVER] Resolved $VAR reference from settings_<agent>.json against process.env.');
661
806
  }
662
807
  const detected = detectTokenProvider(resolvedValue);
663
- 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.');
808
+ 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.');
664
809
  return { tokenEnvKey: t.key, tokenValue: resolvedValue, detectedProvider: detected.provider, source: 'settings-explicit' };
665
810
  }
666
811
  const candidates = [];
@@ -689,7 +834,7 @@ export class NousHermesRunner {
689
834
  // Pass B: re-map the first candidate to the right provider env-var name.
690
835
  const first = candidates[0];
691
836
  if (first.detected.provider !== 'unknown' && first.detected.envKey !== first.key) {
692
- logger.info({ agentName, sourceKey: first.key, detectedProvider: first.detected.provider, remappedTo: first.detected.envKey }, '[SUBTILISATION] Token prefix detected provider mismatch — re-mapping env var.');
837
+ 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.');
693
838
  return {
694
839
  tokenEnvKey: first.detected.envKey,
695
840
  tokenValue: first.value,
@@ -930,6 +1075,65 @@ export class NousHermesRunner {
930
1075
  // edits were being silently deleted after each spawn.
931
1076
  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).');
932
1077
  }
1078
+ let effectiveHermesHome = sharedHome;
1079
+ if (agentName && enabledInSettings.length > 0) {
1080
+ try {
1081
+ const runsDir = path.join(sharedHome, 'runs');
1082
+ if (!fs.existsSync(runsDir))
1083
+ fs.mkdirSync(runsDir, { recursive: true });
1084
+ const runHome = path.join(runsDir, agentName);
1085
+ if (!fs.existsSync(runHome))
1086
+ fs.mkdirSync(runHome, { recursive: true });
1087
+ // Copy auth.json if exists
1088
+ const sharedAuth = path.join(sharedHome, 'auth.json');
1089
+ const runAuth = path.join(runHome, 'auth.json');
1090
+ if (fs.existsSync(sharedAuth)) {
1091
+ fs.copyFileSync(sharedAuth, runAuth);
1092
+ }
1093
+ // Robust directory junction/symlink helper
1094
+ const linkDirRobust = (target, source) => {
1095
+ let exists = false;
1096
+ let stats = null;
1097
+ try {
1098
+ stats = fs.lstatSync(target);
1099
+ exists = true;
1100
+ }
1101
+ catch {
1102
+ // target does not exist
1103
+ }
1104
+ if (exists) {
1105
+ logger.debug({ target, isJunction: stats ? (stats.isSymbolicLink() || stats.isDirectory()) : false }, '[HERMES_HOME] Target directory/link already exists. Skipping link creation.');
1106
+ }
1107
+ else {
1108
+ logger.info({ target, source }, '[HERMES_HOME] Creating junction/symbolic link.');
1109
+ if (process.platform === 'win32') {
1110
+ execSync(`cmd /c mklink /J "${target}" "${source}"`);
1111
+ }
1112
+ else {
1113
+ fs.symlinkSync(source, target);
1114
+ }
1115
+ }
1116
+ };
1117
+ // Link agents directory
1118
+ const sharedAgents = path.join(sharedHome, 'agents');
1119
+ const runAgents = path.join(runHome, 'agents');
1120
+ linkDirRobust(runAgents, sharedAgents);
1121
+ // Link sessions directory
1122
+ const sharedSessions = path.join(sharedHome, 'sessions');
1123
+ const runSessions = path.join(runHome, 'sessions');
1124
+ linkDirRobust(runSessions, sharedSessions);
1125
+ // Generate filtered config.yaml
1126
+ const sharedConfig = path.join(sharedHome, 'config.yaml');
1127
+ const runConfig = path.join(runHome, 'config.yaml');
1128
+ const filteredConfig = filterConfigYaml(sharedConfig, enabledInSettings);
1129
+ fs.writeFileSync(runConfig, filteredConfig, 'utf8');
1130
+ effectiveHermesHome = runHome;
1131
+ logger.info({ agentName, runHome, enabledMcp: enabledInSettings }, '[HERMES_HOME] Isolated agent Hermes Home and filtered config.yaml successfully.');
1132
+ }
1133
+ catch (e) {
1134
+ logger.warn({ error: e }, 'Failed to setup isolated run home; falling back to sharedHome');
1135
+ }
1136
+ }
933
1137
  // AbortSignal
934
1138
  if (options.signal?.aborted)
935
1139
  return Promise.reject(new Error('ABORTED'));
@@ -939,10 +1143,27 @@ export class NousHermesRunner {
939
1143
  let retryCount = 0;
940
1144
  const maxRetries = getAvailableFallbacks().length + 1;
941
1145
  let currentSessionId = sessionId;
942
- const safeResolve = (v) => { if (!resolved) {
943
- resolved = true;
944
- resolve(v);
945
- } };
1146
+ const abortListener = () => {
1147
+ if (currentChildRef) {
1148
+ killProcessTree(currentChildRef).then(() => {
1149
+ cleanupTmpFiles();
1150
+ safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
1151
+ });
1152
+ }
1153
+ else {
1154
+ cleanupTmpFiles();
1155
+ safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
1156
+ }
1157
+ };
1158
+ const safeResolve = (v) => {
1159
+ if (!resolved) {
1160
+ resolved = true;
1161
+ if (options.signal) {
1162
+ options.signal.removeEventListener('abort', abortListener);
1163
+ }
1164
+ resolve(v);
1165
+ }
1166
+ };
946
1167
  const cleanupTmpFiles = () => {
947
1168
  for (const f of [tmpSettingsPath, tmpMcpPath]) {
948
1169
  if (f && fs.existsSync(f)) {
@@ -1031,12 +1252,11 @@ export class NousHermesRunner {
1031
1252
  // etc. relative to HERMES_HOME. Setting it to the per-agent home would
1032
1253
  // tell Hermes "this IS the Hermes root" and make it look for config.yaml
1033
1254
  // IN the agent dir — wrong layout.
1034
- const sharedHermesHome = getSharedHermesHome();
1035
1255
  const child = spawn(hermesBin, cleanArgs, {
1036
1256
  cwd, shell: false, windowsHide: true,
1037
1257
  env: {
1038
1258
  ...spawnEnv,
1039
- HERMES_HOME: sharedHermesHome,
1259
+ HERMES_HOME: effectiveHermesHome,
1040
1260
  ...(isVenvInstall
1041
1261
  ? {
1042
1262
  VIRTUAL_ENV: venvRoot,
@@ -1067,8 +1287,8 @@ export class NousHermesRunner {
1067
1287
  void appendOutput(child.pid, chunk, configPath);
1068
1288
  void appendLiveOutput(child.pid, chunk);
1069
1289
  }
1070
- if (stdout.length + chunk.length > MAX_BUF)
1071
- stdout = stdout.slice(-MAX_BUF);
1290
+ if (stdout.length + chunk.length > this.MAX_BUF)
1291
+ stdout = stdout.slice(-this.MAX_BUF);
1072
1292
  else
1073
1293
  stdout += chunk;
1074
1294
  if (!silent && agentName)
@@ -1076,8 +1296,8 @@ export class NousHermesRunner {
1076
1296
  });
1077
1297
  child.stderr?.on('data', (d) => {
1078
1298
  const chunk = d.toString();
1079
- if (stderr.length + chunk.length > MAX_BUF)
1080
- stderr = stderr.slice(-MAX_BUF);
1299
+ if (stderr.length + chunk.length > this.MAX_BUF)
1300
+ stderr = stderr.slice(-this.MAX_BUF);
1081
1301
  else
1082
1302
  stderr += chunk;
1083
1303
  if (!silent && agentName)
@@ -1137,13 +1357,9 @@ export class NousHermesRunner {
1137
1357
  });
1138
1358
  });
1139
1359
  };
1140
- options.signal?.addEventListener('abort', () => {
1141
- if (currentChildRef)
1142
- killProcessTree(currentChildRef).then(() => {
1143
- cleanupTmpFiles();
1144
- safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
1145
- });
1146
- });
1360
+ if (options.signal) {
1361
+ options.signal.addEventListener('abort', abortListener);
1362
+ }
1147
1363
  let firstToken;
1148
1364
  try {
1149
1365
  firstToken = getTokenForIndex(0);