Skip to content

Commit 692c894

Browse files
Ark0Nclaude
andcommitted
fix: correct process tree detection and prevent timer starvation
1. Rewrote getActiveChildProcesses() to use a single `ps --ppid` call instead of two-level pgrep. The pane PID is typically claude itself (bash exec'd into it), not a bash wrapper — so direct children of pane_pid ARE the tool processes. 2. Added timer restart in tryStartAiCheck() when skipping due to child processes. Without this, the pre-filter and no-output timers (both one-shot) would never fire again, permanently stalling idle detection for sessions with silent long-running processes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ad0acb6 commit 692c894

File tree

2 files changed

+23
-48
lines changed

2 files changed

+23
-48
lines changed

src/respawn-controller.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2079,11 +2079,14 @@ export class RespawnController extends EventEmitter {
20792079
}
20802080

20812081
// Check for active child processes (bash tools, test suites, builds, etc.)
2082+
// These may produce no terminal output, so restart timers to retry periodically.
20822083
const activeProcesses = this.session.getActiveChildProcesses();
20832084
if (activeProcesses.length > 0) {
20842085
const names = activeProcesses.map((p) => p.command).join(', ');
20852086
this.log(`Skipping AI check - ${activeProcesses.length} active child process(es): ${names}`);
20862087
this.logAction('detection', `Skipped AI check: child processes running (${names})`);
2088+
this.startNoOutputTimer();
2089+
this.startPreFilterTimer();
20872090
return;
20882091
}
20892092

src/session.ts

Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)