@@ -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 )
0 commit comments