overmind-mcp
Version:
Orchestrateur universel agents IA multi-modeles via MCP. Inclut le protocole 'Custom-Nickname' pour identifier vos agents avec des surnoms originaux (The Chaos Prophet, Shadow Sniper, etc.), l'isolation mémoire (Private Memory Context) et le support pour
655 lines • 35.6 kB
JavaScript
import fs from 'fs';
import path from 'path';
import os from 'os';
import { fileURLToPath } from 'url';
import { spawn, exec } from 'child_process';
import { CONFIG, resolveConfigPath, getWorkspaceDir } from '../lib/config.js';
import { getLastSessionId, saveSessionId } from '../lib/sessions.js';
import { interpolateEnvVars } from '../lib/envUtils.js';
import { resolveModel } from '../lib/modelMapping.js';
import { withSpan } from '../lib/telemetry.js';
import { loadEnvQuietly } from '../lib/loadEnv.js';
import { registerProcess, linkSessionToPid, appendOutput, updateProcessStatus, } from '../lib/processRegistry.js';
import { registerLiveAgent, appendLiveOutput, setLiveStatus, unregisterLiveAgent, } from '../lib/agent_lifecycle.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Sur Windows, `child.kill()` ne tue que cmd.exe (le wrapper) — claude.exe
// devient orphelin et garde la session bound au token initial côté provider.
// On utilise `taskkill /F /T /PID` pour propager le kill au sous-arbre,
// puis on attend l'event 'exit' avant de respawn.
const killProcessTree = (child) => {
return new Promise((resolve) => {
if (!child || child.exitCode !== null || child.killed) {
resolve();
return;
}
let settled = false;
const finish = () => {
if (settled)
return;
settled = true;
resolve();
};
child.once('exit', finish);
if (process.platform === 'win32' && child.pid) {
exec(`taskkill /F /T /PID ${child.pid}`, () => {
// taskkill peut échouer si le process est déjà mort — on s'appuie sur 'exit'
});
}
else {
try {
child.kill('SIGTERM');
}
catch {
// Ignored
}
setTimeout(() => {
if (child.exitCode === null && !child.killed) {
try {
child.kill('SIGKILL');
}
catch {
// Ignored
}
}
}, 2000);
}
// Filet de sécurité : si 'exit' ne se déclenche pas (process zombie),
// on débloque le respawn après 5s plutôt que de bloquer indéfiniment.
setTimeout(finish, 5000);
});
};
export class ClaudeRunner {
config;
timeoutMs;
constructor() {
this.config = CONFIG.CLAUDE;
this.timeoutMs = CONFIG.TIMEOUT_MS || 900000;
}
async runAgent(options) {
if (options.agentName) {
// Inline validation — prevents path traversal on settings_${agentName}.json
if (!/^[a-zA-Z0-9_-]+$/.test(options.agentName)) {
return {
result: '',
error: `INVALID_AGENT_NAME: '${options.agentName}' contains invalid characters. Only [a-zA-Z0-9_-] allowed.`,
};
}
}
const { prompt, agentName, autoResume } = options;
let { sessionId } = options;
const { CORE, PERMISSIONS, PATHS } = this.config;
const agentCustomEnv = {};
// Load environment variables FIRST before any processing
const workspaceEnvPath = path.resolve(options.configPath || getWorkspaceDir(), '.env');
loadEnvQuietly(workspaceEnvPath);
// Also load from Workflow directory as fallback
const workflowEnvPath = path.resolve(__dirname, '../../.env');
loadEnvQuietly(workflowEnvPath);
if (agentName) {
agentCustomEnv.OVERMIND_AGENT_NAME = agentName;
}
// --- Auto Resume ---
if (autoResume && agentName && !sessionId) {
const lastId = await getLastSessionId(agentName, options.configPath, 'claude');
if (lastId) {
sessionId = lastId;
if (!options.silent) {
console.log(`[ClaudeRunner] Auto-resume session: ${sessionId}`);
}
}
}
let settingsPath = resolveConfigPath(PATHS.SETTINGS, options.configPath);
if (agentName) {
const settingsDir = path.dirname(PATHS.SETTINGS);
const specificSettingsPath = resolveConfigPath(path.join(settingsDir, `settings_${agentName}.json`), options.configPath);
if (!fs.existsSync(specificSettingsPath)) {
return {
result: '',
error: `INVALID_AGENT: Agent "${agentName}" non trouvé.`,
};
}
settingsPath = specificSettingsPath;
}
let mcpPath = resolveConfigPath(PATHS.MCP, options.configPath);
let tmpMcpPathToDelete = null;
let tmpSettingsPathToDelete = null;
let customTimeoutMs = this.timeoutMs;
if (agentName) {
try {
const agentSettingsPath = resolveConfigPath(path.join(path.dirname(PATHS.SETTINGS), `settings_${agentName}.json`), options.configPath);
if (fs.existsSync(agentSettingsPath)) {
let settings = JSON.parse(fs.readFileSync(agentSettingsPath, 'utf8'));
// --- New interpolation logic ---
settings = interpolateEnvVars(settings);
// 1. Create a temporary settings file with interpolated values
const tmpSettingsPath = path.join(path.dirname(agentSettingsPath), `settings_${agentName}_tmp.json`);
fs.writeFileSync(tmpSettingsPath, JSON.stringify(settings, null, 2));
settingsPath = tmpSettingsPath;
tmpSettingsPathToDelete = tmpSettingsPath;
if (settings.env) {
Object.assign(agentCustomEnv, settings.env);
if (settings.env.AGENT_TIMEOUT_MS || settings.env.API_TIMEOUT_MS) {
const timeoutValue = settings.env.AGENT_TIMEOUT_MS || settings.env.API_TIMEOUT_MS;
customTimeoutMs = parseInt(timeoutValue, 10) || customTimeoutMs;
}
if (!options.model && settings.env.MODEL) {
agentCustomEnv.ANTHROPIC_MODEL = settings.env.MODEL;
}
}
const agentMcpPath = resolveConfigPath(path.join(path.dirname(PATHS.SETTINGS), `.mcp.${agentName}.json`));
if (fs.existsSync(agentMcpPath)) {
mcpPath = agentMcpPath;
}
else if (settings.enableAllProjectMcpServers === false &&
Array.isArray(settings.enabledMcpjsonServers)) {
if (fs.existsSync(mcpPath)) {
const fullMcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
const filteredMcp = { mcpServers: {} };
for (const serverName of settings.enabledMcpjsonServers) {
if (fullMcp.mcpServers && fullMcp.mcpServers[serverName]) {
filteredMcp.mcpServers[serverName] = fullMcp.mcpServers[serverName];
}
}
const tmpMcpPath = path.join(path.dirname(agentSettingsPath), `mcp_${agentName}_tmp.json`);
fs.writeFileSync(tmpMcpPath, JSON.stringify(filteredMcp, null, 2));
mcpPath = tmpMcpPath;
tmpMcpPathToDelete = tmpMcpPath;
}
}
}
}
catch (e) {
console.error(`[ClaudeRunner] [WARN] Error processing agent settings: ${e}`);
}
}
const argsSpawn = [];
if (CORE)
argsSpawn.push(...CORE.split(' ').filter(Boolean));
if (PERMISSIONS)
argsSpawn.push(...PERMISSIONS.split(' ').filter(Boolean));
argsSpawn.push('--settings', settingsPath);
argsSpawn.push('--mcp-config', mcpPath);
argsSpawn.push('--output-format', 'json');
let modelUsed = options.model;
if (!modelUsed && agentCustomEnv.ANTHROPIC_MODEL) {
modelUsed = agentCustomEnv.ANTHROPIC_MODEL;
}
// Remember original value (nickname or raw model) for display
const originalModel = modelUsed ?? '';
// Resolve nickname → real model ID before calling the API
if (modelUsed) {
modelUsed = resolveModel(modelUsed);
}
if (sessionId)
argsSpawn.push('--resume', sessionId);
if (modelUsed)
argsSpawn.push('--model', modelUsed);
if (agentName)
argsSpawn.push('--name', agentName);
// ───────────────────────────────────────────────────────────────────────────
// 🔄 FALLBACK TOKEN RETRY LOGIC
//
// Overmind lit les tokens fallback depuis agentCustomEnv (résolus depuis $VAR).
// Si une erreur 401 (auth) survient, on tente chaque fallback séquentiellement :
// AUTH_FALLBACK_1 → AUTH_FALLBACK_2 → AUTH_FALLBACK_3
//
// Settings exemple :
// { "env": { "ANTHROPIC_AUTH_TOKEN": "$ANTHROPIC_AUTH_FALLBACK_1" } }
// ───────────────────────────────────────────────────────────────────────────
const FALLBACK_KEYS = ['AUTH_FALLBACK_1', 'AUTH_FALLBACK_2', 'AUTH_FALLBACK_3'];
const TOKEN_KEYS = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_AUTH_TOKEN_E'];
/**
* Vérifie si une erreur est retryable (fallback recommended).
* 401 = auth error (token invalide/expiré)
* 429 = rate limit / quota exhausted
* 500/502/503 = server error
*/
const isRetryableError = (stderr, jsonEnv) => {
const lower = stderr.toLowerCase();
const status = jsonEnv?.api_error_status;
if (status === 401)
return true;
if (status === 429)
return true;
if (status === 500 || status === 502 || status === 503)
return true;
return (lower.includes('401') ||
lower.includes('unauthorized') ||
lower.includes('invalid api key') ||
lower.includes('api key invalid') ||
lower.includes('authentication failed') ||
lower.includes('auth error') ||
lower.includes('invalid authentication') ||
lower.includes('429') ||
lower.includes('rate limit') ||
lower.includes('quota exhausted') ||
lower.includes('limit exhausted') ||
lower.includes('503') ||
lower.includes('service unavailable') ||
lower.includes('500') ||
lower.includes('internal server error'));
};
/**
* Extrait les tokens fallback disponibles depuis agentCustomEnv.
* Retourne un tableau de { key, value } pour chaque fallback non vide.
*/
const getAvailableFallbacks = () => {
const fallbacks = [];
for (const key of FALLBACK_KEYS) {
const val = agentCustomEnv[key];
if (val && typeof val === 'string' && val.length > 0) {
fallbacks.push({ key, value: val });
}
}
return fallbacks;
};
/**
* Détermine quel token utiliser selon l'index de retry.
* - index 0 = tentative initiale → use primary token (ANTHROPIC_AUTH_TOKEN)
* - index 1+ = retry → skip primary, use fallbacks directly
*/
const getTokenForIndex = (index) => {
if (index === 0) {
// Tentative initiale : utiliser le primary token
// NOTE: si la valeur est un $VAR non résolu (interpolateEnvVars n'a pas trouvé
// la variable dans process.env à ce moment), on le passe quand même à spawnWithToken
// qui fera la résolution finale via process.env.
for (const tk of TOKEN_KEYS) {
const val = agentCustomEnv[tk];
if (val && typeof val === 'string' && val.length > 0) {
return { tokenEnvKey: tk, tokenValue: val };
}
}
// Aucun primary token trouvé — retourner null plutôt que de tomber dans les fallbacks
return null;
}
// Retry (index >= 1) : skip primary, use fallbacks directly
const fallbacks = getAvailableFallbacks();
const fallbackIndex = index - 1; // index 1 → fallback[0] (AUTH_FALLBACK_1)
if (fallbackIndex < fallbacks.length) {
return {
tokenEnvKey: fallbacks[fallbackIndex].key,
tokenValue: fallbacks[fallbackIndex].value,
};
}
return null;
};
// ─── AbortSignal support ─────────────────────────────────────────────────────
let currentChildRef = null;
if (options.signal?.aborted) {
return Promise.reject(new Error('ABORTED'));
}
options.signal?.addEventListener('abort', () => {
if (currentChildRef)
void killProcessTree(currentChildRef);
});
const runImpl = async (span) => {
span.setAttribute('agentName', agentName || '');
span.setAttribute('model', modelUsed || '');
span.setAttribute('runner', 'claude');
return new Promise((resolve) => {
let resolved = false;
let retryCount = 0;
const maxRetries = getAvailableFallbacks().length + 1; // primary + fallbacks
currentChildRef = null;
let currentStderr = '';
let currentStdout = '';
const MAX_BUF = 10 * 1024 * 1024;
let currentSessionId = sessionId;
let earlyExitTriggered = false; // Prevent double-exit on early retry
const safeResolve = (value) => {
if (!resolved) {
resolved = true;
resolve(value);
}
};
const triggerRetry = async (targetRetryCount) => {
if (earlyExitTriggered)
return;
earlyExitTriggered = true;
if (hardTimeoutTimer)
clearTimeout(hardTimeoutTimer);
if (killTimer)
clearTimeout(killTimer);
// Attendre la mort effective de l'arbre (cmd.exe + claude.exe sur Win)
// avant de respawn, sinon le nouveau process tape sur la même session
// encore "vivante" côté provider → 429 fantôme.
if (currentChildRef) {
await killProcessTree(currentChildRef);
}
retryCount = targetRetryCount;
const tokenInfo = getTokenForIndex(retryCount);
if (!options.silent) {
process.stderr.write(`\n\x1b[41m\x1b[37m[ClaudeRunner] 🔄 Retry ${retryCount}/${maxRetries} avec ${tokenInfo?.tokenEnvKey || 'UNKNOWN'}...\x1b[0m\n`);
}
setImmediate(() => spawnWithToken(tokenInfo));
};
const cleanupTmpFiles = () => {
if (tmpMcpPathToDelete && fs.existsSync(tmpMcpPathToDelete)) {
try {
fs.unlinkSync(tmpMcpPathToDelete);
}
catch {
// Ignored
}
}
if (tmpSettingsPathToDelete && fs.existsSync(tmpSettingsPathToDelete)) {
try {
fs.unlinkSync(tmpSettingsPathToDelete);
}
catch {
// Ignored
}
}
};
let killTimer = null;
let hardTimeoutTimer = null;
/**
* Fonction centrale qui spawn le processus Claude avec le bon token.
* Appelé initialement et après chaque retry.
*/
const spawnWithToken = (tokenInfo) => {
// Nettoyer les listeners/timers de la tentative précédente
if (hardTimeoutTimer) {
clearTimeout(hardTimeoutTimer);
hardTimeoutTimer = null;
}
if (killTimer) {
clearTimeout(killTimer);
killTimer = null;
}
// Construire l'env avec le bon token
// NOTE: Overmind gère la substitution des variables $VAR dans les settings.
// Les fallback tokens (AUTH_FALLBACK_1/2/3) sont résolus ici pour le retry 401.
const spawnEnv = {
...process.env,
...agentCustomEnv,
};
// [FIX] Force-injecter ANTHROPIC_BASE_URL et ANTHROPIC_MODEL depuis les
// settings de l'agent AVANT le spread process.env. Sinon, un .env
// corrompu (ex: ANTHROPIC_BASE_URL=https://api.minimax.com avec typo
// minimax au lieu de minimaxi) pollue le spawn et Claude CLI ne peut
// pas joindre l'API. Les settings de l'agent sont la source de vérité.
const CRITICAL_KEYS = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL', 'ANTHROPIC_PROVIDER'];
for (const k of CRITICAL_KEYS) {
// Priority order: agentCustomEnv > process.env (skip if not set)
// Si l'env de l'agent a la valeur, l'utiliser. Sinon garder process.env
// MAIS: si process.env contient une valeur "connue cassée" (ex: api.minimax.com
// au lieu de api.minimaxi.com), forcer la résolution depuis l'agent ou zéro.
if (agentCustomEnv[k] && typeof agentCustomEnv[k] === 'string') {
let resolved = agentCustomEnv[k];
if (resolved.startsWith('$')) {
const envKey = resolved.slice(1);
resolved = process.env[envKey] || '';
}
if (resolved && resolved !== spawnEnv[k]) {
// Override process.env (même si process.env a déjà une valeur)
spawnEnv[k] = resolved;
}
}
// Si process.env a api.minimax.com (typo) ET l'agent a $ANTHROPIC_BASE_URL_M
// (= api.minimaxi.com), l'override ci-dessus aura déjà corrigé.
// Sinon, si aucune résolution, nettoyer la valeur cassée.
if (k === 'ANTHROPIC_BASE_URL' && spawnEnv[k] === 'https://api.minimax.com/anthropic') {
// Force fallback to minimaxi if agent settings have _M suffix
const mKey = k + '_M';
if (process.env[mKey]) {
spawnEnv[k] = process.env[mKey];
}
}
}
if (tokenInfo) {
// Remplacer le token actif par celui du fallback
// NOTE: Les tokens peuvent encore contenir des $VAR non résolus
// (interpolateEnvVars n'a pas trouvé ces vars dans process.env au moment du load).
// On résout ici via process.env (qui a été peuplé par loadEnvQuietly).
for (const tk of TOKEN_KEYS) {
delete spawnEnv[tk];
}
let resolvedToken = tokenInfo.tokenValue;
if (resolvedToken.startsWith('$')) {
const envKey = resolvedToken.slice(1);
resolvedToken = process.env[envKey] || resolvedToken;
}
// Le Claude CLI lit ANTHROPIC_AUTH_TOKEN — on injecte toujours sous ce nom,
// peu importe que tokenInfo vienne du primary ou d'un AUTH_FALLBACK_n.
spawnEnv['ANTHROPIC_AUTH_TOKEN'] = resolvedToken;
}
currentStderr = '';
currentStdout = '';
// [V16-NOPOPUP] Spawn claude.exe directly — no cmd.exe, no popup
const claudeExe = process.platform === 'win32'
? process.env.CLAUDE_BIN ||
path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe')
: 'claude';
const command = claudeExe;
const spawnArgs = [...argsSpawn, '-p'];
if (!options.silent) {
const tokenLabel = tokenInfo ? ` (token: ${tokenInfo.tokenEnvKey})` : '';
process.stderr.write(`\n\x1b[33m[ClaudeRunner]${tokenLabel} Spawning Claude CLI...\x1b[0m\n`);
}
currentChildRef = spawn(command, spawnArgs, {
cwd: options.cwd || process.cwd(),
windowsHide: true,
env: spawnEnv,
shell: true, // [V16-CLEAN] Résout claude via PATH
signal: options.signal,
});
// Register: disk persistence + in-memory lifecycle
if (currentChildRef.pid) {
const pid = currentChildRef.pid;
void registerProcess(pid, {
agentName: agentName || '',
runner: 'claude',
configPath: options.configPath,
});
registerLiveAgent({
pid,
runner: 'claude',
agentName: agentName || '',
sessionId: currentSessionId || '',
cleanupFn: async () => {
if (currentChildRef)
await killProcessTree(currentChildRef);
},
childRef: currentChildRef,
});
// Cleanup on exit: lifecycle state only (setLiveStatus + unregister).
// processRegistry.updateProcessStatus is handled by the 'close' handler below.
currentChildRef.once('exit', (code) => {
const exitCode = code ?? null;
setLiveStatus(pid, code === 0 ? 'done' : 'failed', exitCode);
unregisterLiveAgent(pid);
});
}
if (currentChildRef.stdout) {
currentChildRef.stdout.on('data', (d) => {
const chunk = d.toString();
if (currentChildRef && currentChildRef.pid && chunk) {
appendLiveOutput(currentChildRef.pid, chunk);
void appendOutput(currentChildRef.pid, chunk, options.configPath);
}
if (currentStdout.length + chunk.length > MAX_BUF)
currentStdout = currentStdout.slice(-MAX_BUF);
else
currentStdout += chunk;
if (agentName && !options.silent) {
process.stderr.write(`[ClaudeRunner:${agentName}] ${chunk}`);
}
});
}
if (currentChildRef.stderr) {
currentChildRef.stderr.on('data', (d) => {
const chunk = d.toString();
if (currentChildRef && currentChildRef.pid && chunk) {
appendLiveOutput(currentChildRef.pid, chunk);
void appendOutput(currentChildRef.pid, chunk, options.configPath);
}
if (currentStderr.length + chunk.length > MAX_BUF)
currentStderr = currentStderr.slice(-MAX_BUF);
else
currentStderr += chunk;
if (agentName && !options.silent) {
process.stderr.write(`[ClaudeRunner:${agentName}:ERR] ${chunk}`);
}
});
}
if (currentChildRef.stdin) {
currentChildRef.stdin.write(prompt);
currentChildRef.stdin.end();
}
const timeout = setTimeout(() => {
if (currentChildRef && currentChildRef.stdin && !currentChildRef.stdin.destroyed) {
try {
currentChildRef.stdin.write('\n');
if (!options.silent) {
process.stderr.write(`\n\x1b[33m[ClaudeRunner] [WARN] Agent stagnant (${customTimeoutMs}ms). Envoi d'un keep-alive (\\n)...\x1b[0m\n`);
}
}
catch (_e) {
// Ignore
}
}
const hardTimeoutDelay = CONFIG.HARD_TIMEOUT_MS || 60000;
hardTimeoutTimer = setTimeout(() => {
if (currentChildRef)
void killProcessTree(currentChildRef);
cleanupTmpFiles();
safeResolve({
result: '',
error: 'HARD_TIMEOUT',
rawOutput: currentStdout + currentStderr,
});
}, hardTimeoutDelay);
}, customTimeoutMs);
currentChildRef.on('error', (err) => {
clearTimeout(timeout);
if (hardTimeoutTimer)
clearTimeout(hardTimeoutTimer);
cleanupTmpFiles();
safeResolve({ result: '', error: `SPAWN_ERROR: ${err.message}`, rawOutput: '' });
});
currentChildRef.on('close', async (code) => {
clearTimeout(timeout);
if (hardTimeoutTimer)
clearTimeout(hardTimeoutTimer);
const fullRaw = currentStdout + (currentStderr ? `\n\n--- STDERR ---\n${currentStderr}` : '');
// ─── Parser le JSON en premier (pour extraire api_error_status) ───
let jsonEnvelope = null;
const trimmedStdout = currentStdout.trim();
try {
jsonEnvelope = JSON.parse(trimmedStdout);
}
catch {
const lastBrace = trimmedStdout.lastIndexOf('}');
const firstBrace = trimmedStdout.lastIndexOf('{', lastBrace);
if (firstBrace !== -1 && lastBrace !== -1) {
try {
jsonEnvelope = JSON.parse(trimmedStdout.substring(firstBrace, lastBrace + 1));
}
catch {
// Ignored
}
}
}
// ─── Fallback retry ─────────────────────────────────────────────────
// Sur 401/429/5xx, on tue l'arbre du process courant via killProcessTree
// (taskkill /F /T sur Windows pour ne pas laisser claude.exe orphelin),
// puis on respawn un nouveau noeud avec --resume + le token fallback
// suivant (AUTH_FALLBACK_1 → 2 → 3).
// ───────────────────────────────────────────────────────────────────
const FALLBACK_RETRY_ENABLED = true;
const isRetryable = isRetryableError(currentStderr, jsonEnvelope);
const hasRetryableStatus = jsonEnvelope !== null &&
(jsonEnvelope.api_error_status === 401 ||
jsonEnvelope.api_error_status === 429 ||
jsonEnvelope.api_error_status === 500 ||
jsonEnvelope.api_error_status === 502 ||
jsonEnvelope.api_error_status === 503);
const isFailure = FALLBACK_RETRY_ENABLED && ((code !== 0 && isRetryable) || hasRetryableStatus);
if (isFailure) {
if (retryCount < maxRetries) {
triggerRetry(retryCount + 1);
return;
}
else {
if (!options.silent) {
process.stderr.write(`\n\x1b[41m\x1b[37m[ClaudeRunner] ❌ Tous les tokens fallback épuisés. Error retryable finale.\x1b[0m\n`);
}
// Status updated by the 'exit' handler registered above
safeResolve({
result: '',
error: 'RETRYABLE_ERROR_ALL_FALLBACKS_EXHAUSTED',
rawOutput: fullRaw,
});
return;
}
}
cleanupTmpFiles();
try {
if (jsonEnvelope) {
let foundSessionId = currentSessionId;
if (jsonEnvelope.session_id && agentName) {
foundSessionId = jsonEnvelope.session_id;
currentSessionId = foundSessionId;
await saveSessionId(agentName, jsonEnvelope.session_id, options.configPath, 'claude');
if (currentChildRef?.pid) {
void linkSessionToPid(jsonEnvelope.session_id, currentChildRef.pid, options.configPath);
}
}
if (currentChildRef?.pid) {
void updateProcessStatus(currentChildRef.pid, code === 0 ? 'done' : 'failed', code ?? null, options.configPath);
}
return safeResolve({
result: jsonEnvelope.reply ||
jsonEnvelope.result ||
currentStdout.trim(),
sessionId: foundSessionId,
rawOutput: currentStdout,
model: modelUsed ?? undefined,
nickname: originalModel !== modelUsed ? originalModel : undefined,
});
}
if (code === 0) {
if (currentChildRef?.pid) {
void updateProcessStatus(currentChildRef.pid, 'done', code, options.configPath);
}
return safeResolve({
result: currentStdout.trim(),
sessionId: currentSessionId,
rawOutput: currentStdout,
model: modelUsed ?? undefined,
nickname: originalModel !== modelUsed ? originalModel : undefined,
});
}
if (currentChildRef?.pid) {
void updateProcessStatus(currentChildRef.pid, 'failed', code, options.configPath);
}
safeResolve({
result: '',
error: code !== 0 ? `EXIT_CODE_${code}` : 'JSON_PARSE_ERROR',
rawOutput: fullRaw,
});
}
catch (error) {
safeResolve({
result: '',
error: `INTERNAL_ERROR: ${error instanceof Error ? error.message : String(error)}`,
rawOutput: fullRaw,
});
}
});
};
// ─── Démarrage initial avec le primary token ───
spawnWithToken(getTokenForIndex(0));
});
};
return withSpan('claude.runAgent', runImpl, {
agentName: agentName || '',
model: modelUsed || '',
runner: 'claude',
});
}
}
//# sourceMappingURL=ClaudeRunner.js.map