@@ -2,7 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process";
22import { realpathSync } from "node:fs" ;
33import { StringDecoder } from "node:string_decoder" ;
44import type { ToolPlugin } from "@intx/tools-posix" ;
5- import type { ShellOutputFeed } from "../session/shell-output-feed.js" ;
5+ import type { ShellOutputFeedMap } from "../session/shell-output-feed.js" ;
66import {
77 killProcessTree ,
88 type BackgroundShellRegistry ,
@@ -321,20 +321,43 @@ export async function runGuardedShell(
321321
322322 let settled = false ;
323323 let timer : ReturnType < typeof setTimeout > | undefined ;
324+ let emitTimer : ReturnType < typeof setTimeout > | undefined ;
324325
325326 // Live-output cadence for the transcript's shell tail (when wired).
326327 const decoder = new StringDecoder ( "utf8" ) ;
327328 let pendingOutput = "" ;
328329 let lastEmitAt = 0 ;
330+ const clearEmitTimer = ( ) : void => {
331+ if ( emitTimer !== undefined ) {
332+ clearTimeout ( emitTimer ) ;
333+ emitTimer = undefined ;
334+ }
335+ } ;
329336 const emitPendingOutput = ( final : boolean ) : void => {
330- if ( onOutput === undefined || pendingOutput . length === 0 ) return ;
331- if ( ! final && Date . now ( ) - lastEmitAt < SHELL_FEED_EMIT_MS ) return ;
337+ if ( onOutput === undefined || pendingOutput . length === 0 ) {
338+ if ( final ) clearEmitTimer ( ) ;
339+ return ;
340+ }
341+ if ( ! final ) {
342+ const wait = SHELL_FEED_EMIT_MS - ( Date . now ( ) - lastEmitAt ) ;
343+ if ( wait > 0 ) {
344+ if ( emitTimer === undefined ) {
345+ emitTimer = setTimeout ( ( ) => {
346+ emitTimer = undefined ;
347+ emitPendingOutput ( false ) ;
348+ } , wait ) ;
349+ }
350+ return ;
351+ }
352+ }
353+ clearEmitTimer ( ) ;
332354 lastEmitAt = Date . now ( ) ;
333355 onOutput ( pendingOutput ) ;
334356 pendingOutput = "" ;
335357 } ;
336358 const clearTimer = ( ) => {
337359 if ( timer !== undefined ) clearTimeout ( timer ) ;
360+ clearEmitTimer ( ) ;
338361 } ;
339362
340363 const settle = ( err ?: Error ) => {
@@ -436,9 +459,9 @@ export interface ShellGuardPluginOptions {
436459 // Live getter for the background-shell registry. Unwired (undefined result)
437460 // makes `background: true` fail closed: nothing spawns, no handle returns.
438461 getBackgroundShellRegistry ?: ( ) => BackgroundShellRegistry | undefined ;
439- // Live getter for the bounded shell-output feed the transcript polls for a
440- // running command's live tail. Unwired, the tail is simply not painted.
441- getShellOutputFeed ?: ( ) => ShellOutputFeed | undefined ;
462+ // Live getter for the per-call bounded shell-output feeds the transcript
463+ // polls for a running command's live tail. Unwired, the tail is simply not painted.
464+ getShellOutputFeeds ?: ( ) => ShellOutputFeedMap | undefined ;
442465}
443466
444467function resolveAllowOutsideCwd (
@@ -569,8 +592,8 @@ export function shellGuardPlugin(
569592 } ;
570593 }
571594 const wrappedCommand = wrapCommandWithPwdProbe ( command ) ;
572- const feed = options . getShellOutputFeed ?.( ) ;
573- if ( feed !== undefined ) feed . clear ( ) ;
595+ const feeds = options . getShellOutputFeeds ?.( ) ;
596+ const feed = feeds ?. forCall ( call . id ) ;
574597 try {
575598 const { output, exitCode, timedOut, outputTruncated } =
576599 await runGuardedShell (
@@ -627,6 +650,8 @@ export function shellGuardPlugin(
627650 content : err instanceof Error ? err . message : String ( err ) ,
628651 isError : true ,
629652 } ;
653+ } finally {
654+ feeds ?. drop ( call . id ) ;
630655 }
631656 } ) . catch ( ( err : unknown ) => ( {
632657 callId : call . id ,
0 commit comments