mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
655 lines
25 KiB
TypeScript
655 lines
25 KiB
TypeScript
/** Executes prepared CLI backend runs and owns their queue and resource lifecycle. */
|
|
import crypto from "node:crypto";
|
|
import { parse as parseSemver } from "semver";
|
|
import { assertAgentRunLifecycleGenerationCurrent } from "../../infra/agent-events.js";
|
|
import { isTruthyEnvValue } from "../../infra/env.js";
|
|
import { formatErrorMessage, toErrorObject } from "../../infra/errors.js";
|
|
import { sanitizeHostExecEnv } from "../../infra/host-env-security.js";
|
|
import { compareValidSemver } from "../../infra/semver.js";
|
|
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
|
import type { CliBackendThinkingLevel } from "../../plugins/cli-backend.types.js";
|
|
import { applySkillEnvOverridesFromSnapshot } from "../../skills/runtime/env-overrides.js";
|
|
import { appendBootstrapPromptWarning } from "../bootstrap-budget.js";
|
|
import {
|
|
fingerprintCliRuntimeArtifact,
|
|
resolveCliRuntimeOwnerFingerprint,
|
|
} from "../cli-auth-epoch.js";
|
|
import { resolveCliExecutableIdentity } from "../cli-executable-identity.js";
|
|
import type { CliOutput } from "../cli-output-contracts.js";
|
|
import {
|
|
detectImageReferences,
|
|
hasHydratableMediaImages,
|
|
} from "../embedded-agent-runner/run/images.js";
|
|
import { applyPluginTextReplacements } from "../plugin-text-transforms.js";
|
|
import { prepareCliBundleMcpCaptureAttempt } from "./bundle-mcp.js";
|
|
import { buildClaudeOwnerKey, closeClaudeSession } from "./claude-live-registry.js";
|
|
import { acceptsClaudeLive } from "./claude-live-session-policy.js";
|
|
import { prepareClaudeCliSkillsPlugin } from "./claude-skills-plugin.js";
|
|
import { executeDeps } from "./execute-deps.js";
|
|
import { createCliEventHandlers } from "./execute-events.js";
|
|
import {
|
|
buildCliExecLogLine,
|
|
CLAUDE_SELECTED_AUTH_ENV_KEYS,
|
|
CLI_BACKEND_PRESERVE_ENV,
|
|
logCliInvocation,
|
|
NODE_CLAUDE_FORWARD_ENV_KEYS,
|
|
parseCliBackendPreserveEnv,
|
|
resolveNodeClaudeAuthEnv,
|
|
} from "./execute-logging.js";
|
|
import {
|
|
createCliAbortError,
|
|
resolveNodeClaudeTarget,
|
|
stripGatewayLocalClaudeArgs,
|
|
} from "./execute-node-claude.js";
|
|
import { executeCliProcess } from "./execute-process.js";
|
|
import { createCliToolTracking } from "./execute-tool-tracking.js";
|
|
import {
|
|
buildCliArgs,
|
|
enqueueCliRun,
|
|
prepareCliPromptImagePayload,
|
|
resolveCliNoOutputTimeoutMs,
|
|
resolveCliRunQueueKey,
|
|
resolveCliRunTimeoutOverrideMs,
|
|
resolvePromptInput,
|
|
resolveSessionIdToSend,
|
|
resolveSystemPromptUsage,
|
|
} from "./helpers.js";
|
|
import {
|
|
cliBackendLog,
|
|
CLI_BACKEND_LOG_OUTPUT_ENV,
|
|
LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV,
|
|
} from "./log.js";
|
|
import { createClaudeCliModelCallDiagnostics } from "./model-call-diagnostics.js";
|
|
import { buildCliBackendToolAvailability } from "./tool-policy.js";
|
|
import type { PreparedCliRunContext } from "./types.js";
|
|
|
|
function normalizeCliBackendThinkingLevel(
|
|
level: PreparedCliRunContext["params"]["thinkLevel"],
|
|
): CliBackendThinkingLevel | undefined {
|
|
return level === "ultra" ? "max" : level;
|
|
}
|
|
|
|
function exactToolAvailabilityError(params: {
|
|
code: "unsupported" | "runtime-unavailable";
|
|
isolatedCompletion: boolean;
|
|
message: string;
|
|
}): Error {
|
|
if (!params.isolatedCompletion) {
|
|
return new Error(params.message);
|
|
}
|
|
const error = new Error(params.message) as Error & { code: typeof params.code };
|
|
error.name = "IsolatedCompletionRuntimeError";
|
|
error.code = params.code;
|
|
return error;
|
|
}
|
|
|
|
function assertExactToolAvailabilityRuntimeVersion(params: {
|
|
backendId: string;
|
|
policy: NonNullable<
|
|
PreparedCliRunContext["backendResolved"]["runtimeArtifact"]
|
|
>["exactToolAvailabilityVersionPolicy"];
|
|
executableIdentity: Awaited<ReturnType<typeof resolveCliExecutableIdentity>>;
|
|
isolatedCompletion: boolean;
|
|
}): void {
|
|
const artifact = params.executableIdentity?.runtimeArtifact;
|
|
const packageVersion = artifact?.kind === "package-tree" ? artifact.packageVersion : undefined;
|
|
const parsedVersion = packageVersion ? parseSemver(packageVersion) : null;
|
|
const prereleaseChannel = parsedVersion?.prerelease[0];
|
|
const minimumVersion =
|
|
parsedVersion?.prerelease.length === 0
|
|
? params.policy?.stableMinimum
|
|
: typeof prereleaseChannel === "string"
|
|
? params.policy?.prereleaseMinimums?.[prereleaseChannel]
|
|
: undefined;
|
|
const comparison =
|
|
packageVersion && minimumVersion ? compareValidSemver(packageVersion, minimumVersion) : null;
|
|
if (comparison !== null && comparison >= 0) {
|
|
return;
|
|
}
|
|
throw exactToolAvailabilityError({
|
|
code: "unsupported",
|
|
isolatedCompletion: params.isolatedCompletion,
|
|
message: `CLI backend ${params.backendId} requires a supported package version for exact per-run tool availability${minimumVersion ? ` (requires >=${minimumVersion}` : " (unsupported release line"}${packageVersion ? `; found ${packageVersion})` : ")"}`,
|
|
});
|
|
}
|
|
|
|
type ExecutePreparedCliRunOptions = {
|
|
onPhase?: (phase: "send" | "resolve" | "cleanup") => void;
|
|
};
|
|
|
|
/** Executes a prepared CLI run context and returns normalized CLI output. */
|
|
export async function executePreparedCliRun(
|
|
context: PreparedCliRunContext,
|
|
cliSessionIdToUse?: string,
|
|
options?: ExecutePreparedCliRunOptions,
|
|
): Promise<CliOutput> {
|
|
const params = context.params;
|
|
if (params.abortSignal?.aborted) {
|
|
throw createCliAbortError();
|
|
}
|
|
const backend = context.preparedBackend.backend;
|
|
const nodePlacement = resolveNodeClaudeTarget(context);
|
|
const { sessionId: resolvedSessionId, isNew } = resolveSessionIdToSend({
|
|
backend,
|
|
cliSessionId: cliSessionIdToUse,
|
|
});
|
|
const useResume = Boolean(
|
|
cliSessionIdToUse && resolvedSessionId && backend.resumeArgs && backend.resumeArgs.length > 0,
|
|
);
|
|
const resendSystemPromptForSoftResume = context.reusableCliSession.mode === "reuse-with-drift";
|
|
const systemPromptArg = resolveSystemPromptUsage({
|
|
backend,
|
|
isNewSession: isNew || resendSystemPromptForSoftResume,
|
|
systemPrompt: context.systemPrompt,
|
|
});
|
|
const shouldSendSystemPrompt =
|
|
systemPromptArg &&
|
|
(!useResume || backend.systemPromptWhen === "always" || resendSystemPromptForSoftResume);
|
|
const systemPromptFile =
|
|
!nodePlacement && shouldSendSystemPrompt
|
|
? await executeDeps.writeCliSystemPromptFile({ backend, systemPrompt: systemPromptArg })
|
|
: undefined;
|
|
const nodeSystemPrompt = nodePlacement && shouldSendSystemPrompt ? systemPromptArg : undefined;
|
|
|
|
const basePrompt = cliSessionIdToUse
|
|
? params.prompt
|
|
: (context.openClawHistoryPrompt ?? params.prompt);
|
|
let prompt = applyPluginTextReplacements(
|
|
appendBootstrapPromptWarning(basePrompt, context.bootstrapPromptWarningLines, {
|
|
preserveExactPrompt: context.heartbeatPrompt,
|
|
}),
|
|
context.backendResolved.textTransforms?.input,
|
|
);
|
|
if (
|
|
nodePlacement &&
|
|
((params.images?.length ?? 0) > 0 ||
|
|
hasHydratableMediaImages(params.media) ||
|
|
(params.imagePrompt ? detectImageReferences(params.imagePrompt).length > 0 : false))
|
|
) {
|
|
throw new Error("paired-node Claude CLI sessions do not support attachments or images");
|
|
}
|
|
const imagePayload = nodePlacement
|
|
? { prompt, imagePaths: [] as string[], cleanupImages: async () => {} }
|
|
: await prepareCliPromptImagePayload({
|
|
backend,
|
|
prompt,
|
|
imagePrompt: params.imagePrompt,
|
|
workspaceDir: context.workspaceDir,
|
|
localRoots: getAgentScopedMediaLocalRoots(params.config ?? {}, params.agentId),
|
|
images: params.images,
|
|
imageOrder: params.imageOrder,
|
|
media: params.media,
|
|
});
|
|
prompt = imagePayload.prompt;
|
|
const { argsPrompt, stdin } = resolvePromptInput({ backend, prompt });
|
|
const baseArgs = useResume ? (backend.resumeArgs ?? backend.args ?? []) : (backend.args ?? []);
|
|
const resolvedArgs = useResume
|
|
? baseArgs.map((entry) => entry.replaceAll("{sessionId}", resolvedSessionId ?? ""))
|
|
: baseArgs;
|
|
const fallbackClaudeSkillsPlugin =
|
|
!nodePlacement && context.claudeSkillsPluginArgs === undefined
|
|
? await prepareClaudeCliSkillsPlugin({
|
|
backendId: context.backendResolved.id,
|
|
skillsSnapshot: params.skillsSnapshot,
|
|
})
|
|
: undefined;
|
|
let fallbackClaudeSkillsPluginCleanupOwned = false;
|
|
const claudeSkillsPluginArgs = nodePlacement
|
|
? []
|
|
: (context.claudeSkillsPluginArgs ?? fallbackClaudeSkillsPlugin?.args ?? []);
|
|
const baseArgsWithSkills =
|
|
claudeSkillsPluginArgs.length > 0 ? [...resolvedArgs, ...claudeSkillsPluginArgs] : resolvedArgs;
|
|
const resolvedExecutionArgs = context.backendResolved.resolveExecutionArgs?.({
|
|
config: params.config,
|
|
workspaceDir: context.workspaceDir,
|
|
provider: params.provider,
|
|
modelId: context.modelId,
|
|
authProfileId: context.effectiveAuthProfileId,
|
|
thinkingLevel: normalizeCliBackendThinkingLevel(params.thinkLevel),
|
|
executionMode: params.executionMode ?? "agent",
|
|
// Node runs project the native subset only: gateway-loopback MCP tools do
|
|
// not exist on the node, and auto-approval must not cross that boundary.
|
|
toolAvailability: params.cliToolAvailability
|
|
? buildCliBackendToolAvailability(
|
|
nodePlacement
|
|
? { native: params.cliToolAvailability.native, openClaw: [] }
|
|
: params.cliToolAvailability,
|
|
)
|
|
: undefined,
|
|
useResume,
|
|
baseArgs: baseArgsWithSkills,
|
|
});
|
|
if (
|
|
params.cliToolAvailability &&
|
|
context.backendResolved.toolAvailabilityEnforcement === "execution-args" &&
|
|
!resolvedExecutionArgs
|
|
) {
|
|
throw new Error(
|
|
`CLI backend ${context.backendResolved.id} did not enforce exact per-run tool availability`,
|
|
);
|
|
}
|
|
const executionBaseArgs = nodePlacement
|
|
? stripGatewayLocalClaudeArgs(resolvedExecutionArgs ?? baseArgsWithSkills)
|
|
: (resolvedExecutionArgs ?? baseArgsWithSkills);
|
|
const args = buildCliArgs({
|
|
backend: nodePlacement
|
|
? { ...backend, systemPromptArg: undefined, systemPromptFileArg: undefined }
|
|
: backend,
|
|
baseArgs: Array.from(executionBaseArgs),
|
|
modelId: context.normalizedModel,
|
|
sessionId: resolvedSessionId,
|
|
systemPrompt: nodePlacement ? undefined : systemPromptArg,
|
|
systemPromptFilePath: systemPromptFile?.filePath,
|
|
imagePaths: imagePayload.imagePaths,
|
|
promptArg: argsPrompt,
|
|
useResume,
|
|
forkResume: params.forkCliSessionOnResume,
|
|
resumeAt: params.cliSessionResumeAt,
|
|
sendSystemPromptOnResume: resendSystemPromptForSoftResume,
|
|
});
|
|
|
|
const claudeOwnerKey = buildClaudeOwnerKey({
|
|
agentAccountId: params.agentAccountId,
|
|
agentId: params.agentId,
|
|
authProfileId: context.effectiveAuthProfileId,
|
|
sessionId: params.sessionId,
|
|
sessionKey: params.sessionKey,
|
|
});
|
|
const queueKey = resolveCliRunQueueKey({
|
|
backendId: context.backendResolved.id,
|
|
liveSession: backend.liveSession,
|
|
serialize: backend.serialize,
|
|
runId: params.runId,
|
|
workspaceDir: context.workspaceDir,
|
|
cliSessionId: useResume ? resolvedSessionId : undefined,
|
|
ownerKey: claudeOwnerKey,
|
|
});
|
|
const useManagedClaudeLiveSession = acceptsClaudeLive(context) && !params.onSuccessfulAuthBinding;
|
|
// Fresh-session retries invoke this function again. Keep one helper per
|
|
// observable CLI attempt so every started call retains its own terminal event.
|
|
const diagnostics = createClaudeCliModelCallDiagnostics({
|
|
context,
|
|
prompt,
|
|
systemPrompt: systemPromptArg ?? undefined,
|
|
transport: nodePlacement
|
|
? "paired-node-cli"
|
|
: useManagedClaudeLiveSession
|
|
? "stdio-live"
|
|
: "stdio",
|
|
});
|
|
let completedOutput: CliOutput | undefined;
|
|
let executionError: unknown;
|
|
let outerCleanupError: Error | undefined;
|
|
let forkResumeClaimed = false;
|
|
let forkSuccessorObserved = false;
|
|
let forkSuccessorPersistence: Promise<void> | undefined;
|
|
const observeForkSuccessor = (sessionId: string) => {
|
|
if (
|
|
forkSuccessorObserved ||
|
|
!forkResumeClaimed ||
|
|
!resolvedSessionId ||
|
|
sessionId === resolvedSessionId
|
|
) {
|
|
return;
|
|
}
|
|
forkSuccessorObserved = true;
|
|
forkSuccessorPersistence = params.persistCliSessionForkSuccessor?.(sessionId);
|
|
void forkSuccessorPersistence?.catch(() => undefined);
|
|
};
|
|
const finishForkSuccessorPersistence = async () => {
|
|
try {
|
|
await forkSuccessorPersistence;
|
|
} catch (error) {
|
|
forkSuccessorObserved = false;
|
|
throw error;
|
|
}
|
|
};
|
|
const cleanupOuterResource = async (cleanup: (() => Promise<void>) | undefined) => {
|
|
try {
|
|
await cleanup?.();
|
|
} catch (error) {
|
|
if (completedOutput?.didSendViaMessagingTool) {
|
|
cliBackendLog.warn(
|
|
`CLI outer resource cleanup failed after confirmed message delivery: ${formatErrorMessage(error)}`,
|
|
);
|
|
return;
|
|
}
|
|
if (executionError !== undefined) {
|
|
cliBackendLog.warn(
|
|
`CLI outer resource cleanup also failed after run error: ${formatErrorMessage(error)}`,
|
|
);
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
const executeAttempt = async (): Promise<CliOutput> => {
|
|
await context.preparedBackend.beforeExecution?.();
|
|
if (params.abortSignal?.aborted) {
|
|
throw createCliAbortError();
|
|
}
|
|
const cliTurnStartedAt = Date.now();
|
|
const restoreSkillEnv = params.skillsSnapshot
|
|
? applySkillEnvOverridesFromSnapshot({
|
|
snapshot: params.skillsSnapshot,
|
|
config: params.config,
|
|
})
|
|
: undefined;
|
|
let cleanupMcpCaptureAttempt: (() => Promise<void>) | undefined;
|
|
let runOutput: CliOutput | undefined;
|
|
let runError: unknown;
|
|
let runFailed = false;
|
|
const recordRunError = (error: unknown) => {
|
|
if (!runFailed) {
|
|
runFailed = true;
|
|
runError = error;
|
|
}
|
|
};
|
|
const toolTracking = createCliToolTracking(context);
|
|
const events = createCliEventHandlers({
|
|
context,
|
|
toolTracking,
|
|
getRunState: () => ({ failed: runFailed, error: runError }),
|
|
});
|
|
try {
|
|
cliBackendLog.info(
|
|
buildCliExecLogLine({
|
|
provider: params.provider,
|
|
model: context.normalizedModel,
|
|
promptChars: basePrompt.length,
|
|
trigger: params.trigger,
|
|
useResume,
|
|
cliSessionId: cliSessionIdToUse,
|
|
resolvedSessionId,
|
|
reusableSession: context.reusableCliSession,
|
|
hasHistoryPrompt: Boolean(context.openClawHistoryPrompt),
|
|
}),
|
|
);
|
|
const logOutputText =
|
|
isTruthyEnvValue(process.env[CLI_BACKEND_LOG_OUTPUT_ENV]) ||
|
|
isTruthyEnvValue(process.env[LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV]);
|
|
const outputMode = useResume ? (backend.resumeOutput ?? backend.output) : backend.output;
|
|
const initialGatewayCaptureKey =
|
|
useManagedClaudeLiveSession || nodePlacement || !context.mcpDeliveryCapture
|
|
? undefined
|
|
: crypto.randomUUID();
|
|
const mcpCaptureAttempt = nodePlacement
|
|
? { env: {}, cleanup: undefined }
|
|
: await prepareCliBundleMcpCaptureAttempt({
|
|
mode: context.backendResolved.bundleMcpMode,
|
|
backend,
|
|
env: context.preparedBackend.env,
|
|
captureKey: initialGatewayCaptureKey,
|
|
});
|
|
cleanupMcpCaptureAttempt = mcpCaptureAttempt.cleanup;
|
|
const preparedBackendEnv = context.preparedBackend.env ?? {};
|
|
const hasSelectedClaudeAuth =
|
|
Boolean(context.preparedBackend.secretInput) ||
|
|
[...CLAUDE_SELECTED_AUTH_ENV_KEYS].some((key) => Object.hasOwn(preparedBackendEnv, key));
|
|
const selectedClaudeClearEnv = hasSelectedClaudeAuth
|
|
? new Set(backend.clearEnv ?? [])
|
|
: undefined;
|
|
const configuredBackendEnv = Object.fromEntries(
|
|
Object.entries(backend.env ?? {}).filter(([key]) => !selectedClaudeClearEnv?.has(key)),
|
|
);
|
|
const backendEnv = { ...configuredBackendEnv, ...preparedBackendEnv };
|
|
const nodeEnvEntries = Object.entries(preparedBackendEnv).filter(([key]) =>
|
|
NODE_CLAUDE_FORWARD_ENV_KEYS.has(key),
|
|
);
|
|
const nodeEnv = nodePlacement
|
|
? {
|
|
...Object.fromEntries(nodeEnvEntries),
|
|
...resolveNodeClaudeAuthEnv(context),
|
|
}
|
|
: undefined;
|
|
const env = 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) || selectedClaudeClearEnv?.has(key)) {
|
|
delete env[key];
|
|
}
|
|
}
|
|
if (Object.keys(backendEnv).length > 0) {
|
|
Object.assign(
|
|
env,
|
|
sanitizeHostExecEnv({
|
|
baseEnv: {},
|
|
overrides: backendEnv,
|
|
blockPathOverrides: true,
|
|
}),
|
|
);
|
|
}
|
|
Object.assign(env, mcpCaptureAttempt.env);
|
|
// Never mark Claude CLI as host-managed. That marker routes runs into
|
|
// Anthropic's separate host-managed usage tier instead of normal CLI use.
|
|
delete env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST;
|
|
|
|
let executionCommand = backend.command;
|
|
let executionLeadingArgv: readonly string[] = [];
|
|
context.runtimeOwnerFingerprint = undefined;
|
|
context.runtimeArtifactFingerprint = undefined;
|
|
const exactToolAvailabilityVersionPolicy = params.cliToolAvailability
|
|
? context.backendResolved.runtimeArtifact?.exactToolAvailabilityVersionPolicy
|
|
: undefined;
|
|
if (exactToolAvailabilityVersionPolicy && nodePlacement) {
|
|
throw exactToolAvailabilityError({
|
|
code: "unsupported",
|
|
isolatedCompletion: params.isolatedCompletion === true,
|
|
message: `CLI backend ${context.backendResolved.id} cannot verify its exact tool-availability runtime on a paired node`,
|
|
});
|
|
}
|
|
if (
|
|
(params.onSuccessfulAuthBinding || exactToolAvailabilityVersionPolicy) &&
|
|
!nodePlacement
|
|
) {
|
|
const executableIdentity = await resolveCliExecutableIdentity({
|
|
command: backend.command,
|
|
cwd: context.cwd ?? context.workspaceDir,
|
|
env,
|
|
...(context.backendResolved.runtimeArtifact
|
|
? { runtimeArtifact: context.backendResolved.runtimeArtifact }
|
|
: {}),
|
|
});
|
|
if (!executableIdentity) {
|
|
throw exactToolAvailabilityError({
|
|
code: "runtime-unavailable",
|
|
isolatedCompletion:
|
|
params.isolatedCompletion === true &&
|
|
exactToolAvailabilityVersionPolicy !== undefined,
|
|
message: `CLI backend ${context.backendResolved.id} executable cannot be bound to one durable absolute owner`,
|
|
});
|
|
}
|
|
if (exactToolAvailabilityVersionPolicy) {
|
|
assertExactToolAvailabilityRuntimeVersion({
|
|
backendId: context.backendResolved.id,
|
|
policy: exactToolAvailabilityVersionPolicy,
|
|
executableIdentity,
|
|
isolatedCompletion: params.isolatedCompletion === true,
|
|
});
|
|
}
|
|
executionCommand = executableIdentity.invocation.command;
|
|
executionLeadingArgv = executableIdentity.invocation.leadingArgv;
|
|
context.runtimeArtifactFingerprint = fingerprintCliRuntimeArtifact({
|
|
provider: params.provider,
|
|
backendId: context.backendResolved.id,
|
|
executableIdentity,
|
|
});
|
|
if (params.onSuccessfulAuthBinding && !context.authBindingFingerprint) {
|
|
context.runtimeOwnerFingerprint = await resolveCliRuntimeOwnerFingerprint({
|
|
provider: params.provider,
|
|
config: params.config ?? context.contextEngineConfig,
|
|
...(context.agentDir ? { agentDir: context.agentDir } : {}),
|
|
agentId: params.agentId,
|
|
runtimeOwnerId: context.backendResolved.id,
|
|
...(context.effectiveAuthProfileId
|
|
? { authProfileId: context.effectiveAuthProfileId }
|
|
: {}),
|
|
...(context.authBindingSkipsLocalCredential ? { skipLocalCredential: true } : {}),
|
|
runtimeArtifactFingerprint: context.runtimeArtifactFingerprint,
|
|
});
|
|
}
|
|
}
|
|
if (logOutputText) {
|
|
logCliInvocation({
|
|
args,
|
|
command: executionCommand,
|
|
env,
|
|
systemPromptArg: backend.systemPromptArg,
|
|
modelArg: backend.modelArg,
|
|
imageArg: backend.imageArg,
|
|
argsPrompt,
|
|
log: (message) => cliBackendLog.info(message),
|
|
});
|
|
}
|
|
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,
|
|
});
|
|
toolTracking.beginGatewayCapture(initialGatewayCaptureKey);
|
|
runOutput = await executeCliProcess({
|
|
context,
|
|
backend,
|
|
deps: executeDeps,
|
|
events,
|
|
toolTracking,
|
|
diagnostics,
|
|
nodePlacement,
|
|
nodeSystemPrompt,
|
|
nodeEnv: nodeEnv && Object.keys(nodeEnv).length > 0 ? nodeEnv : undefined,
|
|
nodeClearEnv: selectedClaudeClearEnv ? [...selectedClaudeClearEnv] : undefined,
|
|
useManagedClaudeLiveSession,
|
|
useResume,
|
|
cliSessionIdToUse,
|
|
resolvedSessionId,
|
|
executionCommand,
|
|
executionLeadingArgv,
|
|
executionArgs: args,
|
|
env,
|
|
prompt,
|
|
argsPrompt,
|
|
stdin,
|
|
noOutputTimeoutMs,
|
|
outputMode,
|
|
logOutputText,
|
|
cliTurnStartedAt,
|
|
fallbackCleanup: fallbackClaudeSkillsPlugin?.cleanup,
|
|
claimFallbackCleanup: () => {
|
|
fallbackClaudeSkillsPluginCleanupOwned = fallbackClaudeSkillsPlugin !== undefined;
|
|
},
|
|
observeForkSuccessor,
|
|
options,
|
|
});
|
|
} catch (error) {
|
|
recordRunError(error);
|
|
} finally {
|
|
await toolTracking.finishDeliveryTracking({
|
|
useManagedClaudeLiveSession,
|
|
recordRunError,
|
|
});
|
|
toolTracking.finalizeCapture(events.finalizeParsedTools);
|
|
try {
|
|
await cleanupMcpCaptureAttempt?.();
|
|
} catch (error) {
|
|
recordRunError(error);
|
|
}
|
|
try {
|
|
restoreSkillEnv?.();
|
|
} catch (error) {
|
|
recordRunError(error);
|
|
}
|
|
}
|
|
if (runFailed) {
|
|
throw toolTracking.attachDeliveryEvidence(runError);
|
|
}
|
|
if (!runOutput) {
|
|
throw new Error("CLI run completed without output");
|
|
}
|
|
return toolTracking.withExecutionEvidence({
|
|
...runOutput,
|
|
toolSummary: events.getToolSummary(),
|
|
});
|
|
};
|
|
try {
|
|
completedOutput = await enqueueCliRun(queueKey, async () => {
|
|
if (params.abortSignal?.aborted) {
|
|
throw createCliAbortError();
|
|
}
|
|
if (params.lifecycleGeneration) {
|
|
assertAgentRunLifecycleGenerationCurrent(params.lifecycleGeneration);
|
|
}
|
|
diagnostics?.emitStarted();
|
|
if (params.forkCliSessionOnResume && useResume) {
|
|
if (!params.persistCliSessionForkSuccessor) {
|
|
throw new Error("CLI session fork successor persistence is unavailable");
|
|
}
|
|
forkResumeClaimed = (await params.claimCliSessionFork?.()) === true;
|
|
if (!forkResumeClaimed) {
|
|
throw new Error("CLI session fork marker is no longer available");
|
|
}
|
|
// The fork argument only applies at process startup; a cached warm child
|
|
// would run inside the source session. Force a fresh spawn.
|
|
await closeClaudeSession(context, "restart");
|
|
}
|
|
return await executeAttempt();
|
|
});
|
|
if (completedOutput.sessionId) {
|
|
observeForkSuccessor(completedOutput.sessionId);
|
|
}
|
|
await finishForkSuccessorPersistence();
|
|
if (forkResumeClaimed && !forkSuccessorObserved) {
|
|
await params.restoreCliSessionFork?.();
|
|
forkResumeClaimed = false;
|
|
throw new Error("forked CLI session did not report a successor session id");
|
|
}
|
|
} catch (error) {
|
|
executionError = error;
|
|
diagnostics?.emitError(error);
|
|
let failure = error;
|
|
try {
|
|
await finishForkSuccessorPersistence();
|
|
} catch (persistenceError) {
|
|
failure = new AggregateError(
|
|
[error, persistenceError],
|
|
"CLI turn failed and its fork successor could not be persisted",
|
|
);
|
|
}
|
|
if (forkResumeClaimed && !forkSuccessorObserved) {
|
|
await params.restoreCliSessionFork?.();
|
|
}
|
|
throw failure;
|
|
} finally {
|
|
try {
|
|
if (!fallbackClaudeSkillsPluginCleanupOwned) {
|
|
await cleanupOuterResource(fallbackClaudeSkillsPlugin?.cleanup);
|
|
}
|
|
await cleanupOuterResource(systemPromptFile?.cleanup);
|
|
await cleanupOuterResource(imagePayload.cleanupImages);
|
|
} catch (error) {
|
|
outerCleanupError = toErrorObject(error, "CLI outer resource cleanup failed");
|
|
}
|
|
}
|
|
if (outerCleanupError) {
|
|
options?.onPhase?.("cleanup");
|
|
diagnostics?.emitError(outerCleanupError);
|
|
throw outerCleanupError;
|
|
}
|
|
if (!completedOutput) {
|
|
const error = new Error("CLI run completed without output");
|
|
diagnostics?.emitError(error);
|
|
throw error;
|
|
}
|
|
// Success stays provisional until persistence and cleanup finish; otherwise
|
|
// a rejected turn would be exported as completed.
|
|
diagnostics?.emitCompleted(completedOutput);
|
|
return completedOutput;
|
|
}
|