rtk-ai/rtk · error

stdout streaming thread panicked

Error message

stdout streaming thread panicked

What it means

In rtk's unfiltered proxy path, stdout of the spawned child is drained by a dedicated thread that streams bytes into a buffer. After `child.wait()` completes, main.rs joins that thread's JoinHandle; if the thread panicked (the handle returns Err), it maps the panic to `anyhow!("stdout streaming thread panicked")`. This indicates an internal reader-thread failure, not a failure of the proxied command itself.

Solutions

  1. Re-run the command with `rtk proxy` retried once — transient pipe errors often disappear.
  2. Run the underlying command directly (outside rtk) to confirm the command itself works; if it does, report the rtk bug with the repro command.
  3. Check system resource limits (`ulimit -n`, memory) that could break pipe reads in the streaming thread.
  4. Update rtk to the latest version, as streaming-thread panics are typically internal bugs fixed upstream.
Defensive patterns

Strategy: retry

Try / catch

rtk proxy <cmd> || {
  # fall back to running the command directly
  <cmd>
}

Prevention

When it happens

Trigger: The stdout reader thread spawned in the proxy execution path panics while reading from the child's piped stdout (e.g. I/O read error on the pipe or an unwrap inside the thread), so `stdout_handle.join()` at src/main.rs:3175 returns Err and is converted into this anyhow error.

Common situations: Child process closes/steals its own stdout pipe abnormally; OS-level pipe errors under resource exhaustion (fd limits, OOM killer hitting the thread); a bug in the reader thread's unwrapping logic on unusual byte streams.

Related errors


AI-assisted analysis of rtk-ai/rtk@0924356b4c (2026-09-19). Data as JSON: /api/errors/d43c6c66c8b6acc1. Report an issue: GitHub.

Appendix: source

Thrown at src/main.rs:3175

                    }
                    let mut err = std::io::stderr().lock();
                    err.write_all(&buf[..count])?;
                    err.flush()?;
                }

                Ok(captured)
            });

            let status = child
                .0
                .take()
                .context("Child process missing")?
                .wait()
                .context(format!("Failed waiting for command: {}", cmd_name))?;

            let stdout_bytes = stdout_handle
                .join()
                .map_err(|_| anyhow::anyhow!("stdout streaming thread panicked"))??;
            let stderr_bytes = stderr_handle
                .join()
                .map_err(|_| anyhow::anyhow!("stderr streaming thread panicked"))??;

            let stdout = core::utils::decode_process_output(&stdout_bytes);
            let stderr = core::utils::decode_process_output(&stderr_bytes);
            let full_output = format!("{}{}", stdout, stderr);

            // Track usage (input = output since no filtering)
            timer.track(
                &format!("{} {}", cmd_name, cmd_args.join(" ")),
                &format!("rtk proxy {} {}", cmd_name, cmd_args.join(" ")),
                &full_output,
                &full_output,
            );

            core::utils::exit_code_from_status(&status, &cmd_name)
        }

View on GitHub (pinned to 0924356b4c)