@@ -542,8 +542,13 @@ export class Session extends EventEmitter {
542542 }
543543
544544 /**
545- * Check if the Claude CLI process inside this session's tmux pane has active child processes.
546- * This detects running bash tools, test suites, builds, servers, etc. that Claude spawned.
545+ * Check if the session's process tree has active child processes beyond Claude itself.
546+ * Detects running bash tools, test suites, builds, servers, etc. that Claude spawned.
547+ *
548+ * The tmux pane PID is typically "claude" directly (bash exec'd into it). When Claude
549+ * runs a bash tool, it spawns child processes: claude → bash → npm/node/python/etc.
550+ * We check direct children of the pane PID, filtering out "claude" itself (for the rare
551+ * case where bash wraps claude and didn't exec).
547552 *
548553 * Returns an array of {pid, command} for each child process, or empty array if none.
549554 * Returns empty array if no mux session or on error (fail-open to avoid blocking respawn).
@@ -552,62 +557,29 @@ export class Session extends EventEmitter {
552557 if ( ! this . _muxSession ) return [ ] ;
553558
554559 try {
555- // Get direct children of the pane PID (typically just the claude process)
556560 const panePid = this . _muxSession . pid ;
557- const claudeChildren = execSync ( `pgrep -P ${ panePid } ` , {
561+
562+ // Single call: get direct children with their command names
563+ const output = execSync ( `ps -o pid=,comm= --ppid ${ panePid } 2>/dev/null` , {
558564 encoding : 'utf-8' ,
559565 timeout : EXEC_TIMEOUT_MS ,
560566 } ) . trim ( ) ;
561- if ( ! claudeChildren ) return [ ] ;
562-
563- const claudePids = claudeChildren
564- . split ( '\n' )
565- . map ( ( p ) => parseInt ( p , 10 ) )
566- . filter ( ( p ) => ! Number . isNaN ( p ) ) ;
567+ if ( ! output ) return [ ] ;
567568
568- // For each Claude process, check for its children (the actual running tools/processes)
569569 const activeProcesses : { pid : number ; command : string } [ ] = [ ] ;
570- for ( const claudePid of claudePids ) {
571- try {
572- const toolChildren = execSync ( `pgrep -P ${ claudePid } ` , {
573- encoding : 'utf-8' ,
574- timeout : EXEC_TIMEOUT_MS ,
575- } ) . trim ( ) ;
576- if ( ! toolChildren ) continue ;
577-
578- const toolPids = toolChildren
579- . split ( '\n' )
580- . map ( ( p ) => parseInt ( p , 10 ) )
581- . filter ( ( p ) => ! Number . isNaN ( p ) ) ;
582-
583- if ( toolPids . length === 0 ) continue ;
584-
585- // Get command names for the child processes in a single ps call
586- try {
587- const psOutput = execSync ( `ps -o pid=,comm= -p ${ toolPids . join ( ',' ) } 2>/dev/null` , {
588- encoding : 'utf-8' ,
589- timeout : EXEC_TIMEOUT_MS ,
590- } ) . trim ( ) ;
591- for ( const line of psOutput . split ( '\n' ) ) {
592- const match = line . trim ( ) . match ( / ^ ( \d + ) \s + ( .+ ) / ) ;
593- if ( match ) {
594- activeProcesses . push ( { pid : parseInt ( match [ 1 ] , 10 ) , command : match [ 2 ] . trim ( ) } ) ;
595- }
596- }
597- } catch {
598- // ps failed — just record PIDs without command names
599- for ( const pid of toolPids ) {
600- activeProcesses . push ( { pid, command : 'unknown' } ) ;
601- }
602- }
603- } catch {
604- // No children for this Claude process
605- }
570+ for ( const line of output . split ( '\n' ) ) {
571+ const match = line . trim ( ) . match ( / ^ ( \d + ) \s + ( .+ ) / ) ;
572+ if ( ! match ) continue ;
573+ const pid = parseInt ( match [ 1 ] , 10 ) ;
574+ const command = match [ 2 ] . trim ( ) ;
575+ // Skip the claude process itself (pane_pid may be bash wrapping claude)
576+ if ( command === 'claude' ) continue ;
577+ activeProcesses . push ( { pid, command } ) ;
606578 }
607579
608580 return activeProcesses ;
609581 } catch {
610- // pgrep fails with exit code 1 when no matches — that's normal (no children)
582+ // ps returns exit code 1 when no matches — normal (no children)
611583 return [ ] ;
612584 }
613585 }
0 commit comments