UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

632 lines (631 loc) 26 kB
import { t as isTruthyEnvValue } from "./env-Di49gJwo.js"; import { s as sanitizeHostExecEnv } from "./host-env-security-RnaFYKhk.js"; import { a as shouldLogVerbose } from "./globals-GTrXU4s9.js"; import { n as applyPluginTextReplacements } from "./text-transforms.runtime.js"; import { i as emitAgentEvent } from "./agent-events-C1B8VhOg.js"; import { o as requestHeartbeat } from "./heartbeat-wake-YhOjrxkC.js"; import { a as enqueueSystemEvent } from "./system-events-C5WI3S5a.js"; import { n as classifyFailoverReason } from "./errors-B5Da2wE-.js"; import { t as FailoverError, u as resolveFailoverStatus } from "./failover-error-DAwMDmz6.js"; import { n as applySkillEnvOverridesFromSnapshot } from "./env-overrides-DDFJJj9B.js"; import { i as scopedHeartbeatWakeOptionsForPolicy, n as resolveEventSessionRoutingPolicy, t as resolveEventSessionKeyForPolicy } from "./event-session-routing-BJ_2911I.js"; import "./embedded-agent-helpers-7sfpu1OT.js"; import { n as appendBootstrapPromptWarning } from "./bootstrap-budget-bmmzkOyn.js"; import { d as sanitizeToolResult, u as sanitizeToolArgs } from "./embedded-agent-subscribe.tools-CeNC5LI1.js"; import { t as getProcessSupervisor } from "./supervisor-81LOKEtj.js"; import { i as formatCliBackendOutputDigest, r as cliBackendLog } from "./log-RtgQ_MP8.js"; import { a as createCliJsonlStreamingParser, i as shouldUseClaudeLiveSession, o as extractCliErrorMessage, r as runClaudeLiveSessionTurn, s as parseCliOutput } from "./claude-live-session-C2lunJBE.js"; import { c as resolvePromptInput, d as writeCliSystemPromptFile, f as buildCliSupervisorScopeKey, i as enqueueCliRun, l as resolveSessionIdToSend, m as resolveCliRunTimeoutOverrideMs, o as prepareCliPromptImagePayload, p as resolveCliNoOutputTimeoutMs, r as buildCliArgs, s as resolveCliRunQueueKey, t as buildClaudeOwnerKey, u as resolveSystemPromptUsage } from "./helpers-BLGx1Zfi.js"; import { t as prepareClaudeCliSkillsPlugin } from "./claude-skills-plugin-DpYXpxCq.js"; import crypto from "node:crypto"; //#region src/agents/cli-runner/execute.ts /** * Executes prepared CLI backend runs, including env isolation, streaming parse, * live-session routing, and diagnostics. */ const executeDeps = { getProcessSupervisor, enqueueSystemEvent, requestHeartbeat }; const CLI_RUNNER_OUTPUT_TAIL_BYTES = 64 * 1024; const CLI_RUNNER_OUTPUT_PARSE_BYTES = 1024 * 1024; function appendCliOutputTail(tail, chunk) { if (!chunk) return tail; const chunkBuffer = Buffer.from(chunk); if (chunkBuffer.byteLength >= CLI_RUNNER_OUTPUT_TAIL_BYTES) return Buffer.from(chunkBuffer.subarray(chunkBuffer.byteLength - CLI_RUNNER_OUTPUT_TAIL_BYTES)); const next = Buffer.concat([tail, chunkBuffer], tail.byteLength + chunkBuffer.byteLength); if (next.byteLength <= CLI_RUNNER_OUTPUT_TAIL_BYTES) return next; return Buffer.from(next.subarray(next.byteLength - CLI_RUNNER_OUTPUT_TAIL_BYTES)); } function appendCliOutputParseBuffer(buffer, chunk) { if (!chunk) return { buffer, exceeded: false }; const chunkBuffer = Buffer.from(chunk); if (buffer.byteLength + chunkBuffer.byteLength > CLI_RUNNER_OUTPUT_PARSE_BYTES) { const remainingBytes = CLI_RUNNER_OUTPUT_PARSE_BYTES - buffer.byteLength; if (remainingBytes <= 0) return { buffer, exceeded: true }; return { buffer: Buffer.concat([buffer, chunkBuffer.subarray(0, remainingBytes)], CLI_RUNNER_OUTPUT_PARSE_BYTES), exceeded: true }; } return { buffer: Buffer.concat([buffer, chunkBuffer], buffer.byteLength + chunkBuffer.byteLength), exceeded: false }; } function createCliAbortError() { const error = /* @__PURE__ */ new Error("CLI run aborted"); error.name = "AbortError"; return error; } function buildCliLogArgs(params) { const logArgs = []; for (let i = 0; i < params.args.length; i += 1) { const arg = params.args[i] ?? ""; if (arg === params.systemPromptArg) { const systemPromptValue = params.args[i + 1] ?? ""; logArgs.push(arg, `<systemPrompt:${systemPromptValue.length} chars>`); i += 1; continue; } if (arg === params.sessionArg) { logArgs.push(arg, params.args[i + 1] ?? ""); i += 1; continue; } if (arg === params.modelArg) { logArgs.push(arg, params.args[i + 1] ?? ""); i += 1; continue; } if (arg === params.imageArg) { logArgs.push(arg, "<image>"); i += 1; continue; } logArgs.push(arg); } if (params.argsPrompt) { const promptIndex = logArgs.indexOf(params.argsPrompt); if (promptIndex >= 0) logArgs[promptIndex] = `<prompt:${params.argsPrompt.length} chars>`; } return logArgs; } const CLI_ENV_AUTH_LOG_KEYS = [ "AI_GATEWAY_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD", "ANTHROPIC_API_TOKEN", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_CUSTOM_HEADERS", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_UNIX_SOCKET", "AZURE_OPENAI_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST", "OPENAI_API_KEY", "OPENAI_STEIPETE_API_KEY", "OPENROUTER_API_KEY" ]; const CLI_BACKEND_PRESERVE_ENV = "OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV"; function parseCliBackendPreserveEnv(raw) { const trimmed = raw?.trim(); if (!trimmed) return /* @__PURE__ */ new Set(); if (trimmed.startsWith("[")) try { const parsed = JSON.parse(trimmed); return new Set(Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : []); } catch { return /* @__PURE__ */ new Set(); } return new Set(trimmed.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0)); } function listPresentCliAuthEnvKeys(env) { return CLI_ENV_AUTH_LOG_KEYS.filter((key) => { const value = env[key]; return typeof value === "string" && value.length > 0; }); } function formatCliEnvKeyList(keys) { return keys.length > 0 ? keys.join(",") : "none"; } function buildCliEnvMcpLog(childEnv) { return [ `token=${childEnv.OPENCLAW_MCP_TOKEN ? "set" : "missing"}`, `sessionKey=${childEnv.OPENCLAW_MCP_SESSION_KEY ? "set" : "<empty>"}`, `agentId=${childEnv.OPENCLAW_MCP_AGENT_ID || "<empty>"}`, `accountId=${childEnv.OPENCLAW_MCP_ACCOUNT_ID || "<empty>"}`, `messageChannel=${childEnv.OPENCLAW_MCP_MESSAGE_CHANNEL || "<empty>"}` ].join(" "); } function fingerprintCliSessionId(sessionId) { const trimmed = sessionId?.trim(); if (!trimmed) return "none"; return crypto.createHash("sha256").update(trimmed).digest("hex").slice(0, 12); } /** Builds the compact execution summary logged before a CLI backend run. */ function buildCliExecLogLine(params) { const reuseState = params.reusableSessionId ? "reusable" : params.invalidatedReason ? `invalidated:${params.invalidatedReason}` : "none"; return [ `cli exec: provider=${params.provider}`, `model=${params.model}`, `promptChars=${params.promptChars}`, `trigger=${params.trigger ?? "unknown"}`, `useResume=${params.useResume ? "true" : "false"}`, `session=${params.cliSessionId ? "present" : "none"}`, `resumeSession=${params.useResume ? fingerprintCliSessionId(params.resolvedSessionId) : "none"}`, `reuse=${reuseState}`, `historyPrompt=${params.hasHistoryPrompt ? "present" : "none"}` ].join(" "); } /** Summarizes auth-related env keys preserved or cleared for a CLI child process. */ function buildCliEnvAuthLog(childEnv) { const hostKeys = listPresentCliAuthEnvKeys(process.env); const childKeys = listPresentCliAuthEnvKeys(childEnv); const childKeySet = new Set(childKeys); const clearedKeys = hostKeys.filter((key) => !childKeySet.has(key)); return [ `host=${formatCliEnvKeyList(hostKeys)}`, `child=${formatCliEnvKeyList(childKeys)}`, `cleared=${formatCliEnvKeyList(clearedKeys)}` ].join(" "); } /** Executes a prepared CLI run context and returns normalized CLI output. */ async function executePreparedCliRun(context, cliSessionIdToUse) { const params = context.params; if (params.abortSignal?.aborted) throw createCliAbortError(); const backend = context.preparedBackend.backend; const { sessionId: resolvedSessionId, isNew } = resolveSessionIdToSend({ backend, cliSessionId: cliSessionIdToUse }); const useResume = Boolean(cliSessionIdToUse && resolvedSessionId && backend.resumeArgs && backend.resumeArgs.length > 0); const systemPromptArg = resolveSystemPromptUsage({ backend, isNewSession: isNew, systemPrompt: context.systemPrompt }); const systemPromptFile = systemPromptArg && (!useResume || backend.systemPromptWhen === "always") ? await writeCliSystemPromptFile({ backend, systemPrompt: systemPromptArg }) : void 0; const basePrompt = cliSessionIdToUse ? params.prompt : context.openClawHistoryPrompt ?? params.prompt; let prompt = applyPluginTextReplacements(appendBootstrapPromptWarning(basePrompt, context.bootstrapPromptWarningLines, { preserveExactPrompt: context.heartbeatPrompt }), context.backendResolved.textTransforms?.input); const { prompt: promptWithImages, imagePaths, cleanupImages } = await prepareCliPromptImagePayload({ backend, prompt, workspaceDir: context.workspaceDir, images: params.images }); prompt = promptWithImages; const { argsPrompt, stdin } = resolvePromptInput({ backend, prompt }); const stdinPayload = stdin ?? ""; const baseArgs = useResume ? backend.resumeArgs ?? backend.args ?? [] : backend.args ?? []; const resolvedArgs = useResume ? baseArgs.map((entry) => entry.replaceAll("{sessionId}", resolvedSessionId ?? "")) : baseArgs; const fallbackClaudeSkillsPlugin = context.claudeSkillsPluginArgs === void 0 ? await prepareClaudeCliSkillsPlugin({ backendId: context.backendResolved.id, skillsSnapshot: params.skillsSnapshot }) : void 0; let fallbackClaudeSkillsPluginCleanupOwned = false; const claudeSkillsPluginArgs = context.claudeSkillsPluginArgs ?? fallbackClaudeSkillsPlugin?.args ?? []; const baseArgsWithSkills = claudeSkillsPluginArgs.length > 0 ? [...resolvedArgs, ...claudeSkillsPluginArgs] : resolvedArgs; const executionBaseArgs = context.backendResolved.resolveExecutionArgs?.({ config: params.config, workspaceDir: context.workspaceDir, provider: params.provider, modelId: context.modelId, authProfileId: context.effectiveAuthProfileId, thinkingLevel: params.thinkLevel, useResume, baseArgs: baseArgsWithSkills }) ?? baseArgsWithSkills; const args = buildCliArgs({ backend, baseArgs: Array.from(executionBaseArgs), modelId: context.normalizedModel, sessionId: resolvedSessionId, systemPrompt: systemPromptArg, systemPromptFilePath: systemPromptFile?.filePath, imagePaths, promptArg: argsPrompt, useResume }); const queueKey = resolveCliRunQueueKey({ backendId: context.backendResolved.id, serialize: backend.serialize, runId: params.runId, workspaceDir: context.workspaceDir, cliSessionId: useResume ? resolvedSessionId : void 0, ownerKey: buildClaudeOwnerKey({ agentAccountId: params.agentAccountId, agentId: params.agentId, authProfileId: context.effectiveAuthProfileId, sessionId: params.sessionId, sessionKey: params.sessionKey }) }); try { return await enqueueCliRun(queueKey, async () => { const cliTurnStartedAt = Date.now(); const restoreSkillEnv = params.skillsSnapshot ? applySkillEnvOverridesFromSnapshot({ snapshot: params.skillsSnapshot, config: params.config }) : void 0; try { cliBackendLog.info(buildCliExecLogLine({ provider: params.provider, model: context.normalizedModel, promptChars: basePrompt.length, trigger: params.trigger, useResume, cliSessionId: cliSessionIdToUse, resolvedSessionId, reusableSessionId: context.reusableCliSession.sessionId, invalidatedReason: context.reusableCliSession.invalidatedReason, hasHistoryPrompt: Boolean(context.openClawHistoryPrompt) })); const logOutputText = isTruthyEnvValue(process.env["OPENCLAW_CLI_BACKEND_LOG_OUTPUT"]) || isTruthyEnvValue(process.env["OPENCLAW_CLAUDE_CLI_LOG_OUTPUT"]); const env = (() => { const next = sanitizeHostExecEnv({ baseEnv: process.env, blockPathOverrides: true }); const preservedEnv = parseCliBackendPreserveEnv(process.env[CLI_BACKEND_PRESERVE_ENV]); for (const key of backend.clearEnv ?? []) { if (preservedEnv.has(key)) continue; delete next[key]; } if (backend.env && Object.keys(backend.env).length > 0) Object.assign(next, sanitizeHostExecEnv({ baseEnv: {}, overrides: backend.env, blockPathOverrides: true })); Object.assign(next, context.preparedBackend.env); delete next["CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST"]; return next; })(); if (logOutputText) { const logArgs = buildCliLogArgs({ args, systemPromptArg: backend.systemPromptArg, sessionArg: backend.sessionArg, modelArg: backend.modelArg, imageArg: backend.imageArg, argsPrompt }); cliBackendLog.info(`cli argv: ${backend.command} ${logArgs.join(" ")}`); cliBackendLog.info(`cli env auth: ${buildCliEnvAuthLog(env)}`); if (env.OPENCLAW_MCP_TOKEN || env.OPENCLAW_MCP_SESSION_KEY || env.OPENCLAW_MCP_AGENT_ID) cliBackendLog.info(`cli env mcp: ${buildCliEnvMcpLog(env)}`); } const runTimeoutOverrideMs = resolveCliRunTimeoutOverrideMs({ config: params.config, lane: params.lane, timeoutMs: params.timeoutMs, runTimeoutOverrideMs: params.runTimeoutOverrideMs }); const noOutputTimeoutMs = resolveCliNoOutputTimeoutMs({ backend, timeoutMs: params.timeoutMs, runTimeoutOverrideMs, useResume, trigger: params.trigger }); const outputMode = useResume ? backend.resumeOutput ?? backend.output : backend.output; const hasJsonlOutput = outputMode === "jsonl"; let observedCliActivity = false; const emitCliToolUseStart = (event) => { observedCliActivity = true; emitAgentEvent({ runId: params.runId, stream: "tool", data: { phase: "start", name: event.name, toolCallId: event.toolCallId, args: sanitizeToolArgs(event.args) } }); }; const emitCliToolResult = (event) => { observedCliActivity = true; emitAgentEvent({ runId: params.runId, stream: "tool", data: { phase: "result", name: event.name, toolCallId: event.toolCallId, isError: event.isError, result: sanitizeToolResult(event.result) } }); }; let commentaryCounter = 0; const emitCliCommentaryText = (text) => { commentaryCounter += 1; const transformedText = applyPluginTextReplacements(text, context.backendResolved.textTransforms?.output); emitAgentEvent({ runId: params.runId, stream: "item", data: { kind: "preamble", itemId: `commentary-${params.runId}-${commentaryCounter}`, phase: "update", title: "commentary", status: "running", progressText: transformedText } }); }; if (shouldUseClaudeLiveSession(context)) { if (!hasJsonlOutput) throw new Error("Claude live session requires JSONL streaming parser"); params.onExecutionPhase?.({ phase: "process_spawned", provider: params.provider, model: context.modelId, backend: context.backendResolved.id }); fallbackClaudeSkillsPluginCleanupOwned = true; const ownedPreparedBackendCleanup = context.preparedBackend.cleanup; context.preparedBackend.cleanup = void 0; const liveResult = await runClaudeLiveSessionTurn({ context, args, env, prompt, useResume, noOutputTimeoutMs, getProcessSupervisor: executeDeps.getProcessSupervisor, onAssistantDelta: ({ text, delta }) => { if (text || delta) observedCliActivity = true; emitAgentEvent({ runId: params.runId, stream: "assistant", data: { text: applyPluginTextReplacements(text, context.backendResolved.textTransforms?.output), delta: applyPluginTextReplacements(delta, context.backendResolved.textTransforms?.output) } }); }, onToolUseStart: emitCliToolUseStart, onToolResult: emitCliToolResult, onCommentaryText: context.params.emitCommentaryText ? emitCliCommentaryText : void 0, cleanup: async () => { try { await fallbackClaudeSkillsPlugin?.cleanup(); } finally { await ownedPreparedBackendCleanup?.(); } } }); const rawText = liveResult.output.text; return { ...liveResult.output, rawText, finalPromptText: prompt, text: applyPluginTextReplacements(rawText, context.backendResolved.textTransforms?.output) }; } const streamingParser = hasJsonlOutput ? createCliJsonlStreamingParser({ backend, providerId: context.backendResolved.id, onAssistantDelta: ({ text, delta }) => { if (text || delta) observedCliActivity = true; emitAgentEvent({ runId: params.runId, stream: "assistant", data: { text: applyPluginTextReplacements(text, context.backendResolved.textTransforms?.output), delta: applyPluginTextReplacements(delta, context.backendResolved.textTransforms?.output) } }); }, onToolUseStart: emitCliToolUseStart, onToolResult: emitCliToolResult, onCommentaryText: context.params.emitCommentaryText ? emitCliCommentaryText : void 0 }) : null; const supervisor = executeDeps.getProcessSupervisor(); const scopeKey = buildCliSupervisorScopeKey({ backend, backendId: context.backendResolved.id, cliSessionId: useResume ? resolvedSessionId : void 0 }); let stdoutTail = Buffer.alloc(0); let stdoutParseBuffer = Buffer.alloc(0); let stdoutParseExceeded = false; let stderrTail = Buffer.alloc(0); let stderrParseBuffer = Buffer.alloc(0); let stderrParseExceeded = false; params.onExecutionPhase?.({ phase: "process_spawned", provider: params.provider, model: context.modelId, backend: context.backendResolved.id }); const managedRun = await supervisor.spawn({ sessionId: params.sessionId, backendId: context.backendResolved.id, scopeKey, replaceExistingScope: Boolean(useResume && scopeKey), mode: "child", argv: [backend.command, ...args], timeoutMs: params.timeoutMs, noOutputTimeoutMs, cwd: context.cwd ?? context.workspaceDir, env, input: stdinPayload, captureOutput: false, onStdout: (chunk) => { stdoutTail = appendCliOutputTail(stdoutTail, chunk); if (!stdoutParseExceeded) { const nextStdoutParse = appendCliOutputParseBuffer(stdoutParseBuffer, chunk); stdoutParseBuffer = nextStdoutParse.buffer; stdoutParseExceeded = nextStdoutParse.exceeded; } streamingParser?.push(chunk); }, onStderr: (chunk) => { stderrTail = appendCliOutputTail(stderrTail, chunk); if (!stderrParseExceeded) { const nextStderrParse = appendCliOutputParseBuffer(stderrParseBuffer, chunk); stderrParseBuffer = nextStderrParse.buffer; stderrParseExceeded = nextStderrParse.exceeded; } } }); let replyBackendCompleted = false; const replyBackendHandle = params.replyOperation ? { kind: "cli", cancel: () => { managedRun.cancel("manual-cancel"); }, isStreaming: () => !replyBackendCompleted } : void 0; if (replyBackendHandle) params.replyOperation?.attachBackend(replyBackendHandle); const abortManagedRun = () => { managedRun.cancel("manual-cancel"); }; params.abortSignal?.addEventListener("abort", abortManagedRun, { once: true }); if (params.abortSignal?.aborted) abortManagedRun(); let result; try { result = await managedRun.wait(); } finally { replyBackendCompleted = true; if (replyBackendHandle) params.replyOperation?.detachBackend(replyBackendHandle); params.abortSignal?.removeEventListener("abort", abortManagedRun); } streamingParser?.finish(); if (params.abortSignal?.aborted && result.reason === "manual-cancel") throw createCliAbortError(); const stdout = stdoutParseBuffer.toString("utf8").trim(); const stdoutDiagnostic = stdoutTail.toString("utf8").trim(); const stderr = stderrParseBuffer.toString("utf8").trim(); const stderrDiagnostic = stderrTail.toString("utf8").trim(); if (logOutputText) { if (stdoutDiagnostic) cliBackendLog.info(`cli stdout:\n${stdoutDiagnostic}`); if (stderrDiagnostic) cliBackendLog.info(`cli stderr:\n${stderrDiagnostic}`); } if (shouldLogVerbose()) { if (stdoutDiagnostic) cliBackendLog.debug(`cli stdout:\n${stdoutDiagnostic}`); if (stderrDiagnostic) cliBackendLog.debug(`cli stderr:\n${stderrDiagnostic}`); } if (result.exitCode !== 0 || result.reason !== "exit") { if (result.reason === "no-output-timeout" || result.noOutputTimedOut) { const timeoutReason = `CLI produced no output for ${Math.round(noOutputTimeoutMs / 1e3)}s and was terminated.`; cliBackendLog.warn(`cli watchdog timeout: provider=${params.provider} model=${context.modelId} session=${resolvedSessionId ?? params.sessionId} noOutputTimeoutMs=${noOutputTimeoutMs} pid=${managedRun.pid ?? "unknown"}`); const retryableNoOutputTimeout = !observedCliActivity && stdoutDiagnostic.length === 0 && stderrDiagnostic.length === 0; const deferWatchdogNoticeForFreshRetry = retryableNoOutputTimeout && Boolean(cliSessionIdToUse) && Boolean(resolvedSessionId) && Boolean(context.openClawHistoryPrompt) && Boolean(params.sessionKey) && params.timeoutMs - (Date.now() - context.started) > 0; if (params.sessionKey && !deferWatchdogNoticeForFreshRetry) { const stallNotice = [ `CLI agent (${params.provider}) produced no output for ${Math.round(noOutputTimeoutMs / 1e3)}s and was terminated.`, "It may have been waiting for interactive input or an approval prompt.", "For Claude Code, prefer --permission-mode bypassPermissions --print." ].join(" "); const eventRouting = resolveEventSessionRoutingPolicy({ cfg: params.config, sessionKey: params.sessionKey, channel: params.messageProvider, accountId: params.agentAccountId }); executeDeps.enqueueSystemEvent(stallNotice, { sessionKey: resolveEventSessionKeyForPolicy(params.sessionKey, eventRouting) }); executeDeps.requestHeartbeat(scopedHeartbeatWakeOptionsForPolicy(params.sessionKey, { source: "cli-watchdog", intent: "event", reason: "cli:watchdog:stall" }, eventRouting)); } throw new FailoverError(timeoutReason, { reason: "timeout", provider: params.provider, model: context.modelId, sessionId: params.sessionId, lane: params.lane, status: resolveFailoverStatus("timeout"), code: retryableNoOutputTimeout ? "cli_no_output_timeout" : void 0 }); } if (result.reason === "overall-timeout") throw new FailoverError(`CLI exceeded timeout (${Math.round(params.timeoutMs / 1e3)}s) and was terminated.`, { reason: "timeout", provider: params.provider, model: context.modelId, sessionId: params.sessionId, lane: params.lane, status: resolveFailoverStatus("timeout"), code: "cli_overall_timeout" }); const errorCandidates = [ stderr, stdout, stderrDiagnostic, stdoutDiagnostic ].filter((candidate) => candidate.length > 0); const structuredError = errorCandidates.map((candidate) => extractCliErrorMessage(candidate)).find(Boolean) ?? null; let classifiedErrorText = structuredError; let reason = structuredError ? classifyFailoverReason(structuredError, { provider: params.provider }) : null; if (!reason) for (const candidate of errorCandidates) { reason = classifyFailoverReason(candidate, { provider: params.provider }); if (reason) { classifiedErrorText = candidate; break; } } const err = structuredError || classifiedErrorText || errorCandidates[0] || "CLI failed."; reason = reason ?? "unknown"; const status = resolveFailoverStatus(reason); const retryCode = reason === "unknown" && result.reason === "exit" && errorCandidates.length === 0 && !observedCliActivity ? "cli_unknown_empty_failure" : void 0; throw new FailoverError(err, { reason, provider: params.provider, model: context.modelId, sessionId: params.sessionId, lane: params.lane, status, code: retryCode }); } const streamedJsonlOutput = outputMode === "jsonl" ? streamingParser?.getOutput() ?? null : null; if (stdoutParseExceeded && !streamedJsonlOutput) throw new FailoverError(`CLI stdout exceeded ${CLI_RUNNER_OUTPUT_PARSE_BYTES} bytes; refusing to parse truncated output.`, { reason: "format", provider: params.provider, model: context.modelId, sessionId: params.sessionId, lane: params.lane, status: resolveFailoverStatus("format") }); const parsed = streamedJsonlOutput ?? parseCliOutput({ raw: stdout, backend, providerId: context.backendResolved.id, outputMode, fallbackSessionId: resolvedSessionId }); const rawText = parsed.text; cliBackendLog.info(`cli turn: provider=${params.provider} model=${context.modelId} durationMs=${Date.now() - cliTurnStartedAt} ${formatCliBackendOutputDigest(rawText)}`); return { ...parsed, rawText, finalPromptText: prompt, text: applyPluginTextReplacements(rawText, context.backendResolved.textTransforms?.output) }; } finally { restoreSkillEnv?.(); } }); } finally { if (!fallbackClaudeSkillsPluginCleanupOwned) await fallbackClaudeSkillsPlugin?.cleanup(); if (systemPromptFile) await systemPromptFile.cleanup(); if (cleanupImages) await cleanupImages(); } } //#endregion export { executePreparedCliRun };