@@ -290,8 +290,12 @@ export class TerminalProcess extends BaseTerminalProcess {
290290 // Only applies before any data arrives (chunkCount === 0).
291291 const IDLE_TIMEOUT_MS = 3_000
292292
293+ // Hoist nextChunk outside the loop so that re-arming after an idle timeout
294+ // reuses the same pending .next() promise instead of issuing a second call
295+ // while the first is still unresolved (violates the async-iterator protocol
296+ // and silently drops the first output chunk).
297+ let nextChunk = iterator . next ( )
293298 while ( true ) {
294- const nextChunk = iterator . next ( )
295299 const racers : Promise < typeof DONE_SENTINEL | typeof IDLE_SENTINEL | IteratorResult < string > > [ ] = [
296300 nextChunk ,
297301 shellExecutionComplete . then ( ( ) => DONE_SENTINEL as typeof DONE_SENTINEL ) ,
@@ -325,6 +329,7 @@ export class TerminalProcess extends BaseTerminalProcess {
325329 // silent command like `sleep 5` can legitimately produce zero output —
326330 // elapsed time alone is not proof of completion. Re-arm the idle timer
327331 // and keep waiting for a real chunk, the D marker, or the end event.
332+ // Reuse the existing nextChunk promise — do NOT call iterator.next() again.
328333 console . info (
329334 `[Terminal Process] idle timeout fired but shell execution is running — re-arming (${ chunkCount } chunks so far)` ,
330335 )
@@ -396,6 +401,11 @@ export class TerminalProcess extends BaseTerminalProcess {
396401 )
397402 break
398403 }
404+
405+ // Advance to the next chunk only after the current one has been fully
406+ // consumed. Calling iterator.next() here (not at the top of the loop)
407+ // ensures there is never more than one pending .next() call at a time.
408+ nextChunk = iterator . next ( )
399409 }
400410
401411 if ( ! sawEndMarker && ! streamEndedByEvent ) {
@@ -546,7 +556,9 @@ export class TerminalProcess extends BaseTerminalProcess {
546556 return `begin\n${ command } \nend`
547557 }
548558
549- const scriptPath = path . join ( os . tmpdir ( ) , `roo-cmd-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } .sh` )
559+ const scriptDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "roo-cmd-" ) )
560+ this . scriptDir = scriptDir
561+ const scriptPath = path . join ( scriptDir , "cmd.sh" )
550562 fs . writeFileSync ( scriptPath , command , { mode : 0o700 } )
551563 this . scriptPath = scriptPath
552564
@@ -556,6 +568,7 @@ export class TerminalProcess extends BaseTerminalProcess {
556568 }
557569
558570 private scriptPath : string | undefined
571+ private scriptDir : string | undefined
559572
560573 private cleanupScriptFile ( ) {
561574 if ( this . scriptPath ) {
@@ -566,6 +579,14 @@ export class TerminalProcess extends BaseTerminalProcess {
566579 }
567580 this . scriptPath = undefined
568581 }
582+ if ( this . scriptDir ) {
583+ try {
584+ fs . rmdirSync ( this . scriptDir )
585+ } catch {
586+ // Best-effort: ignore if already removed or non-empty.
587+ }
588+ this . scriptDir = undefined
589+ }
569590 }
570591
571592 public override continue ( ) {
0 commit comments