Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit c30924e

Browse files
committed
fix: ensure onCompleted callback finishes before using persistedResult
- Update RooTerminalCallbacks.onCompleted type to allow async callbacks (void | Promise<void>) - Track onCompleted completion with a promise and await it before using persistedResult - This fixes a race condition where exitDetails could be set before the async finalize() completes - Fix test callback to not return assignment value
1 parent 61e7820 commit c30924e

3 files changed

Lines changed: 36 additions & 12 deletions

File tree

src/core/tools/ExecuteCommandTool.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,16 @@ export async function executeCommandInTerminal(
210210
// Bound accumulated output buffer size to prevent unbounded memory growth for long-running commands.
211211
// The interceptor preserves full output; this buffer is only for UI display (100KB limit).
212212
const maxAccumulatedOutputSize = 100_000
213+
214+
// Track when onCompleted callback finishes to avoid race condition.
215+
// The callback is async but Terminal/ExecaTerminal don't await it, so we track completion
216+
// explicitly to ensure persistedResult is set before we use it.
217+
let onCompletedPromise: Promise<void> | undefined
218+
let resolveOnCompleted: (() => void) | undefined
219+
onCompletedPromise = new Promise((resolve) => {
220+
resolveOnCompleted = resolve
221+
})
222+
213223
const callbacks: RooTerminalCallbacks = {
214224
onLine: async (lines: string, process: RooTerminalProcess) => {
215225
accumulatedOutput += lines
@@ -247,18 +257,23 @@ export async function executeCommandInTerminal(
247257
}
248258
},
249259
onCompleted: async (output: string | undefined) => {
250-
// Finalize interceptor and get persisted result.
251-
// We await finalize() to ensure the artifact file is fully flushed
252-
// before we advertise the artifact_id to the LLM.
253-
if (interceptor) {
254-
persistedResult = await interceptor.finalize()
255-
}
260+
try {
261+
// Finalize interceptor and get persisted result.
262+
// We await finalize() to ensure the artifact file is fully flushed
263+
// before we advertise the artifact_id to the LLM.
264+
if (interceptor) {
265+
persistedResult = await interceptor.finalize()
266+
}
256267

257-
// Continue using compressed output for UI display
258-
result = Terminal.compressTerminalOutput(output ?? "")
268+
// Continue using compressed output for UI display
269+
result = Terminal.compressTerminalOutput(output ?? "")
259270

260-
task.say("command_output", result)
261-
completed = true
271+
task.say("command_output", result)
272+
completed = true
273+
} finally {
274+
// Signal that onCompleted has finished, so the main code can safely use persistedResult
275+
resolveOnCompleted?.()
276+
}
262277
},
263278
onShellExecutionStarted: (pid: number | undefined) => {
264279
const status: CommandExecutionStatus = { executionId, status: "started", pid, command }
@@ -348,6 +363,13 @@ export async function executeCommandInTerminal(
348363
// grouping command_output messages despite any gaps anyways).
349364
await delay(50)
350365

366+
// Wait for onCompleted callback to finish if shell execution completed.
367+
// This ensures persistedResult is set before we try to use it, fixing the race
368+
// condition where exitDetails is set (sync) before the async onCompleted finishes.
369+
if (exitDetails && onCompletedPromise) {
370+
await onCompletedPromise
371+
}
372+
351373
if (message) {
352374
const { text, images } = message
353375
await task.say("user_feedback", text, images)

src/integrations/terminal/__tests__/ExecaTerminal.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ describe("ExecaTerminal", () => {
1515

1616
const callbacks: RooTerminalCallbacks = {
1717
onLine: vi.fn(),
18-
onCompleted: (output) => (result = output),
18+
onCompleted: (output) => {
19+
result = output
20+
},
1921
onShellExecutionStarted: vi.fn(),
2022
onShellExecutionComplete: vi.fn(),
2123
}

src/integrations/terminal/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface RooTerminal {
2222

2323
export interface RooTerminalCallbacks {
2424
onLine: (line: string, process: RooTerminalProcess) => void
25-
onCompleted: (output: string | undefined, process: RooTerminalProcess) => void
25+
onCompleted: (output: string | undefined, process: RooTerminalProcess) => void | Promise<void>
2626
onShellExecutionStarted: (pid: number | undefined, process: RooTerminalProcess) => void
2727
onShellExecutionComplete: (details: ExitCodeDetails, process: RooTerminalProcess) => void
2828
onNoShellIntegration?: (message: string, process: RooTerminalProcess) => void

0 commit comments

Comments
 (0)