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) <noreply@anthropic.com>
This commit is contained in:
dave
2026-03-31 14:41:00 +00:00
parent d132ed8e64
commit f16545ec36
+13 -1
View File
@@ -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::io::Result<String>>();
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