import { shouldLogVerbose } from "../../globals.js"; import { isTruthyEnvValue } from "../../infra/env.js"; import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js"; import { sanitizeHostExecEnv } from "../../infra/host-env-security.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; import { getProcessSupervisor } from "../../process/supervisor/index.js"; import { scopedHeartbeatWakeOptions } from "../../routing/session-key.js"; import { prependBootstrapPromptWarning } from "../bootstrap-budget.js"; import { parseCliOutput, type CliOutput } from "../cli-output.js"; import { FailoverError, resolveFailoverStatus } from "../failover-error.js"; import { classifyFailoverReason } from "../pi-embedded-helpers.js"; import { appendImagePathsToPrompt, buildCliSupervisorScopeKey, buildCliArgs, enqueueCliRun, loadPromptRefImages, resolveCliNoOutputTimeoutMs, resolvePromptInput, resolveSessionIdToSend, resolveSystemPromptUsage, writeCliImages, } from "./helpers.js"; import { cliBackendLog, CLI_BACKEND_LOG_OUTPUT_ENV, LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV, } from "./log.js"; import type { PreparedCliRunContext } from "./types.js"; function buildCliLogArgs(params: { args: string[]; systemPromptArg?: string; sessionArg?: string; modelArg?: string; imageArg?: string; argsPrompt?: string; }): string[] { const logArgs: string[] = []; 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, ``); 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, ""); i += 1; continue; } logArgs.push(arg); } if (params.argsPrompt) { const promptIndex = logArgs.indexOf(params.argsPrompt); if (promptIndex >= 0) { logArgs[promptIndex] = ``; } } return logArgs; } export async function executePreparedCliRun( context: PreparedCliRunContext, cliSessionIdToUse?: string, ): Promise { const params = context.params; 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, }); let imagePaths: string[] | undefined; let cleanupImages: (() => Promise) | undefined; let prompt = prependBootstrapPromptWarning(params.prompt, context.bootstrapPromptWarningLines, { preserveExactPrompt: context.heartbeatPrompt, }); const resolvedImages = params.images && params.images.length > 0 ? params.images : await loadPromptRefImages({ prompt, workspaceDir: context.workspaceDir }); if (resolvedImages.length > 0) { const imagePayload = await writeCliImages(resolvedImages); imagePaths = imagePayload.paths; cleanupImages = imagePayload.cleanup; if (!backend.imageArg) { prompt = appendImagePathsToPrompt(prompt, imagePaths); } } 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 args = buildCliArgs({ backend, baseArgs: resolvedArgs, modelId: context.normalizedModel, sessionId: resolvedSessionId, systemPrompt: systemPromptArg, imagePaths, promptArg: argsPrompt, useResume, }); const serialize = backend.serialize ?? true; const queueKey = serialize ? context.backendResolved.id : `${context.backendResolved.id}:${params.runId}`; try { return await enqueueCliRun(queueKey, async () => { cliBackendLog.info( `cli exec: provider=${params.provider} model=${context.normalizedModel} promptChars=${params.prompt.length}`, ); const logOutputText = isTruthyEnvValue(process.env[CLI_BACKEND_LOG_OUTPUT_ENV]) || isTruthyEnvValue(process.env[LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV]); 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(" ")}`); } const env = (() => { const next = sanitizeHostExecEnv({ baseEnv: process.env, overrides: backend.env, blockPathOverrides: true, }); for (const key of backend.clearEnv ?? []) { delete next[key]; } return next; })(); const noOutputTimeoutMs = resolveCliNoOutputTimeoutMs({ backend, timeoutMs: params.timeoutMs, useResume, }); const supervisor = getProcessSupervisor(); const scopeKey = buildCliSupervisorScopeKey({ backend, backendId: context.backendResolved.id, cliSessionId: useResume ? resolvedSessionId : undefined, }); 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.workspaceDir, env, input: stdinPayload, }); const result = await managedRun.wait(); const stdout = result.stdout.trim(); const stderr = result.stderr.trim(); if (logOutputText) { if (stdout) { cliBackendLog.info(`cli stdout:\n${stdout}`); } if (stderr) { cliBackendLog.info(`cli stderr:\n${stderr}`); } } if (shouldLogVerbose()) { if (stdout) { cliBackendLog.debug(`cli stdout:\n${stdout}`); } if (stderr) { cliBackendLog.debug(`cli stderr:\n${stderr}`); } } 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 / 1000)}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"}`, ); if (params.sessionKey) { const stallNotice = [ `CLI agent (${params.provider}) produced no output for ${Math.round(noOutputTimeoutMs / 1000)}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(" "); enqueueSystemEvent(stallNotice, { sessionKey: params.sessionKey }); requestHeartbeatNow( scopedHeartbeatWakeOptions(params.sessionKey, { reason: "cli:watchdog:stall" }), ); } throw new FailoverError(timeoutReason, { reason: "timeout", provider: params.provider, model: context.modelId, status: resolveFailoverStatus("timeout"), }); } if (result.reason === "overall-timeout") { const timeoutReason = `CLI exceeded timeout (${Math.round(params.timeoutMs / 1000)}s) and was terminated.`; throw new FailoverError(timeoutReason, { reason: "timeout", provider: params.provider, model: context.modelId, status: resolveFailoverStatus("timeout"), }); } const err = stderr || stdout || "CLI failed."; const reason = classifyFailoverReason(err) ?? "unknown"; const status = resolveFailoverStatus(reason); throw new FailoverError(err, { reason, provider: params.provider, model: context.modelId, status, }); } return parseCliOutput({ raw: stdout, backend, providerId: context.backendResolved.id, outputMode: useResume ? (backend.resumeOutput ?? backend.output) : backend.output, fallbackSessionId: resolvedSessionId, }); }); } finally { if (cleanupImages) { await cleanupImages(); } } }