openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
632 lines (631 loc) • 26 kB
JavaScript
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 };