fix(coding-agent/exec): improved bash executor abort handling and resource cleanup

- Added abort event listener registration in bash executor to properly handle abort signals and clean up resources in finally block.
- Improved bash tool error handling to distinguish between user-initiated aborts via AbortSignal and other cancellations, throwing ToolAbortError for aborted requests.
- Wrapped bash executor command execution in try-finally block to ensure abort event listeners are properly removed after execution completes.
This commit is contained in:
can1357
2026-02-10 17:21:09 +01:00
parent 9f94d39789
commit c6c27d8ed1
5 changed files with 75 additions and 47 deletions
+57 -43
View File
@@ -73,50 +73,64 @@ export async function executeBash(command: string, options?: BashExecutorOptions
shellSessions.set(sessionKey, shellSession);
}
const result = await shellSession.run(
{
command: finalCommand,
cwd: options?.cwd,
env: options?.env,
timeoutMs: options?.timeout,
signal: options?.signal,
},
(err, chunk) => {
if (!err) {
enqueueChunk(chunk);
}
},
);
await pendingChunks;
// Handle timeout
if (result.timedOut) {
const annotation = options?.timeout
? `Command timed out after ${Math.round(options.timeout / 1000)} seconds`
: "Command timed out";
return {
exitCode: undefined,
cancelled: true,
...(await sink.dump(annotation)),
};
}
// Handle cancellation
if (result.cancelled) {
return {
exitCode: undefined,
cancelled: true,
...(await sink.dump("Command cancelled")),
};
}
// Normal completion
return {
exitCode: result.exitCode,
cancelled: false,
...(await sink.dump()),
const signal = options?.signal;
const abortHandler = () => {
shellSession.abort(signal?.reason instanceof Error ? signal.reason.message : undefined);
};
if (signal) {
signal.addEventListener("abort", abortHandler, { once: true });
}
try {
const result = await shellSession.run(
{
command: finalCommand,
cwd: options?.cwd,
env: options?.env,
timeoutMs: options?.timeout,
signal,
},
(err, chunk) => {
if (!err) {
enqueueChunk(chunk);
}
},
);
await pendingChunks;
// Handle timeout
if (result.timedOut) {
const annotation = options?.timeout
? `Command timed out after ${Math.round(options.timeout / 1000)} seconds`
: "Command timed out";
return {
exitCode: undefined,
cancelled: true,
...(await sink.dump(annotation)),
};
}
// Handle cancellation
if (result.cancelled) {
return {
exitCode: undefined,
cancelled: true,
...(await sink.dump("Command cancelled")),
};
}
// Normal completion
return {
exitCode: result.exitCode,
cancelled: false,
...(await sink.dump()),
};
} finally {
if (signal) {
signal.removeEventListener("abort", abortHandler);
}
}
} finally {
await pendingChunks;
}