From f16545ec36619b510cd17ac818d4efed93a36e2b Mon Sep 17 00:00:00 2001 From: dave Date: Tue, 31 Mar 2026 14:41:00 +0000 Subject: [PATCH] fix: join PTY reader thread before returning to prevent stale fd leak (#453) The reader thread spawned in run_agent_pty_blocking was never joined, leaving a cloned PTY master fd open after the agent exited. When the pipeline restarted the agent on the same worktree, the stale fd from the previous session interfered with the new PTY allocation, causing Claude Code's bundled ripgrep to crash with: fatal runtime error: assertion failed: output.write(&bytes).is_ok() Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/agents/pty.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/agents/pty.rs b/server/src/agents/pty.rs index e2d1417e..ef6fb906 100644 --- a/server/src/agents/pty.rs +++ b/server/src/agents/pty.rs @@ -245,13 +245,16 @@ fn run_agent_pty_blocking( // via recv_timeout: if no output arrives within the configured window // the process is killed and the agent is marked Failed. let (line_tx, line_rx) = std::sync::mpsc::channel::>(); - std::thread::spawn(move || { + let sid_for_reader = story_id.to_string(); + let aname_for_reader = agent_name.to_string(); + let reader_handle = std::thread::spawn(move || { let buf_reader = BufReader::new(reader); for line in buf_reader.lines() { if line_tx.send(line).is_err() { break; } } + slog!("[agent:{sid_for_reader}:{aname_for_reader}] Reader thread exiting"); }); let timeout_dur = if inactivity_timeout_secs > 0 { @@ -424,6 +427,15 @@ fn run_agent_pty_blocking( let _ = child.kill(); let _ = child.wait(); + // Wait for the reader thread to finish so it releases the cloned PTY + // master fd before we return. Without this, the next PTY spawn for the + // same story can collide with a still-open fd from this session (#453). + slog!("[agent:{story_id}:{agent_name}] Waiting for reader thread to exit"); + if let Err(e) = reader_handle.join() { + slog!("[agent:{story_id}:{agent_name}] Reader thread panicked: {e:?}"); + } + slog!("[agent:{story_id}:{agent_name}] Reader thread joined"); + slog!( "[agent:{story_id}:{agent_name}] Done. Session: {:?}", session_id