Skip to content

Commit b2fb25b

Browse files
fix CRITICAL: zombie process on tool execution timeout (ToolExecutionSandbox.ts)
The old code used Promise.race between a streamPromise and a timeout. When timeout won, streamPromise was abandoned but its for-await loop continued indefinitely (zombie). Then a second concurrent for-await stream was started on the same command, causing duplicate/interleaved output. Fix: replaced Promise.race + abandoned promise with explicit AbortController and a single for-await loop that checks abort signal on each iteration. Timeout clears the loop properly. Background task still starts on timeout.
1 parent 391362b commit b2fb25b

1 file changed

Lines changed: 11 additions & 10 deletions

File tree

apps/desktop/src/renderer/runtime/tools/ToolExecutionSandbox.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -252,22 +252,23 @@ export class ToolExecutionSandbox {
252252
let backgrounded = false
253253
let backgroundTaskId: string | null = null
254254

255-
const streamPromise = (async () => {
255+
const abortController = new AbortController()
256+
const timeoutId = setTimeout(() => abortController.abort(), ASSISTANT_BLOCKING_BUDGET_MS)
257+
258+
try {
256259
for await (const event of this.terminalRuntime.runStream(command, cwd, streamOptions)) {
260+
if (abortController.signal.aborted) break
257261
if (event.type === "OUTPUT_LINE" && event.line) {
258262
// Already handled via onOutput callback
259263
}
260264
}
261-
})()
262-
263-
const result = await Promise.race([
264-
streamPromise.then(() => 'COMPLETED' as const),
265-
new Promise<'TIMEOUT'>((resolve) =>
266-
setTimeout(() => resolve('TIMEOUT'), ASSISTANT_BLOCKING_BUDGET_MS),
267-
),
268-
])
265+
} catch {
266+
// Stream may throw on abort — ignore
267+
} finally {
268+
clearTimeout(timeoutId)
269+
}
269270

270-
if (result === 'TIMEOUT') {
271+
if (abortController.signal.aborted && !abortController.signal.reason) {
271272
backgrounded = true
272273
backgroundTaskId = BackgroundTaskManager.getInstance().spawn(
273274
`Command: ${command.slice(0, 80)}`,

0 commit comments

Comments
 (0)