Hmbown/CodeWhale · error

inline stdout logger panicked

Error message

inline stdout logger panicked

What it means

In the inline (non-tmux-proxied) `start` path, the child's stdout is pumped to a log by a dedicated thread; after `child.wait()` the thread is joined and a panic in it produces this anyhow error. It means the inline stdout logger died before finishing, so the lane log is incomplete and `start` aborts instead of recording a normal lane completion.

Solutions

  1. Read the panic message/backtrace emitted when the thread died to identify the actual I/O or logic failure in `stream_child_output`.
  2. Verify the lane log path exists, is writable, and its filesystem has space for the whole command duration.
  3. Check for races where the log file or its directory is cleaned up while the command is still running.
  4. Change `stream_child_output` to propagate errors (return Result) rather than panicking, so the caller gets the underlying I/O error instead of a bare panic marker.
  5. Retry the start once the environment issue is fixed; also confirm `status.success()` handling is skipped as intended when logging_error is set.

Example fix

// before
stream_to_log(&mut stdout, &log).expect("stdout logging failed");
// after
stream_to_log(&mut stdout, &log)
    .with_context(|| format!("log stdout to {}", log.display()))?;
Defensive patterns

Strategy: try-catch

Validate before calling

if let Some(dir) = record.log_path.parent() {
    assert!(dir.exists(), "inline log dir missing: {}", dir.display());
}
assert!(std::fs::OpenOptions::new().append(true).create(true).open(&record.log_path).is_ok());

Type guard

fn join_ok<T>(h: std::thread::JoinHandle<T>) -> Option<T> { h.join().ok() }

Try / catch

match lane_start_inline(spec) {
    Err(e) if e.to_string().contains("inline stdout logger panicked") => {
        eprintln!("stdout logger thread panicked: {e:#}; verify log path and disk space, then retry");
    }
    Err(e) => eprintln!("start failed: {e:#}"),
    Ok(record) => { /* normal path */ }
}

Prevention

When it happens

Trigger: Calling the inline lane `start` with a command whose stdout logger thread (`stream_child_output(stdout, stdout_log, "stdout")`) panics — e.g. unwrap on a failed write/append to `record.log_path` while the command is producing output.

Common situations: Log file's directory removed or permissions changed while the inline command runs; disk full during verbose output; a panic path in `stream_child_output` triggered by unusual output bytes; tmux dry-run roundtrip tests hitting a race where the log handle is closed early.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/3a08be5250cbffe6. Report an issue: GitHub.

Appendix: source

Thrown at crates/lane/src/runtime.rs:1018

            .stdout
            .take()
            .context("inline lane child stdout was not piped")?;
        let stderr = child
            .stderr
            .take()
            .context("inline lane child stderr was not piped")?;
        let stdout_log = record.log_path.clone();
        let stderr_log = record.log_path.clone();
        let stdout_thread =
            thread::spawn(move || stream_child_output(stdout, stdout_log, "stdout"));
        let stderr_thread =
            thread::spawn(move || stream_child_output(stderr, stderr_log, "stderr"));
        let status = child
            .wait()
            .with_context(|| format!("wait for inline command {:?}", spec.command))?;
        let stdout_result = stdout_thread
            .join()
            .map_err(|_| anyhow::anyhow!("inline stdout logger panicked"))?;
        let stderr_result = stderr_thread
            .join()
            .map_err(|_| anyhow::anyhow!("inline stderr logger panicked"))?;
        let logging_error = stdout_result.err().or_else(|| stderr_result.err());

        if status.success() && logging_error.is_none() {
            append_log_event(
                &record.log_path,
                serde_json::json!({"type": "lane_completed", "lane_id": record.id}),
            )?;
            let _ = registry.mark_terminal_if_active(record, LaneStatus::Completed)?;
        } else {
            append_log_event(
                &record.log_path,
                serde_json::json!({
                    "type": "lane_failed",
                    "lane_id": record.id,
                    "status": format!("{status}"),

View on GitHub (pinned to 73e0f67d83)