refactor(agents): dedupe exec spawn and process failures

This commit is contained in:
Peter Steinberger
2026-02-15 14:28:55 +00:00
parent 34b6c743f5
commit 85b267aae9
2 changed files with 37 additions and 62 deletions

View File

@@ -338,6 +338,25 @@ export async function runExecProcess(opts: {
opts.warnings.push(warning);
};
const spawnShellChild = async (
shell: string,
shellArgs: string[],
): Promise<ChildProcessWithoutNullStreams> => {
const { child: spawned } = await spawnWithFallback({
argv: [shell, ...shellArgs, execCommand],
options: {
cwd: opts.workdir,
env: opts.env,
detached: process.platform !== "win32",
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
},
fallbacks: spawnFallbacks,
onFallback: handleSpawnFallback,
});
return spawned as ChildProcessWithoutNullStreams;
};
// `exec` does not currently accept tool-provided stdin content. For non-PTY runs,
// keeping stdin open can cause commands like `wc -l` (or safeBins-hardened segments)
// to block forever waiting for input, leading to accidental backgrounding.
@@ -421,36 +440,12 @@ export async function runExecProcess(opts: {
const warning = `Warning: PTY spawn failed (${errText}); retrying without PTY for \`${opts.command}\`.`;
logWarn(`exec: PTY spawn failed (${errText}); retrying without PTY for "${opts.command}".`);
opts.warnings.push(warning);
const { child: spawned } = await spawnWithFallback({
argv: [shell, ...shellArgs, execCommand],
options: {
cwd: opts.workdir,
env: opts.env,
detached: process.platform !== "win32",
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
},
fallbacks: spawnFallbacks,
onFallback: handleSpawnFallback,
});
child = spawned as ChildProcessWithoutNullStreams;
child = await spawnShellChild(shell, shellArgs);
stdin = child.stdin;
}
} else {
const { shell, args: shellArgs } = getShellConfig();
const { child: spawned } = await spawnWithFallback({
argv: [shell, ...shellArgs, execCommand],
options: {
cwd: opts.workdir,
env: opts.env,
detached: process.platform !== "win32",
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
},
fallbacks: spawnFallbacks,
onFallback: handleSpawnFallback,
});
child = spawned as ChildProcessWithoutNullStreams;
child = await spawnShellChild(shell, shellArgs);
stdin = child.stdin;
maybeCloseNonPtyStdin();
}

View File

@@ -86,6 +86,18 @@ function resolvePollWaitMs(value: unknown) {
return 0;
}
function failText(text: string): AgentToolResult<unknown> {
return {
content: [
{
type: "text",
text,
},
],
details: { status: "failed" },
};
}
export function createProcessTool(
defaults?: ProcessToolDefaults,
// oxlint-disable-next-line typescript/no-explicit-any
@@ -258,26 +270,10 @@ export function createProcessTool(
},
};
}
return {
content: [
{
type: "text",
text: `No session found for ${params.sessionId}`,
},
],
details: { status: "failed" },
};
return failText(`No session found for ${params.sessionId}`);
}
if (!scopedSession.backgrounded) {
return {
content: [
{
type: "text",
text: `Session ${params.sessionId} is not backgrounded.`,
},
],
details: { status: "failed" },
};
return failText(`Session ${params.sessionId} is not backgrounded.`);
}
const pollWaitMs = resolvePollWaitMs(params.timeout);
if (pollWaitMs > 0 && !scopedSession.exited) {
@@ -521,26 +517,10 @@ export function createProcessTool(
case "kill": {
if (!scopedSession) {
return {
content: [
{
type: "text",
text: `No active session found for ${params.sessionId}`,
},
],
details: { status: "failed" },
};
return failText(`No active session found for ${params.sessionId}`);
}
if (!scopedSession.backgrounded) {
return {
content: [
{
type: "text",
text: `Session ${params.sessionId} is not backgrounded.`,
},
],
details: { status: "failed" },
};
return failText(`Session ${params.sessionId} is not backgrounded.`);
}
killSession(scopedSession);
markExited(scopedSession, null, "SIGKILL", "failed");