Hmbown/CodeWhale · error

lane proxy stdout logger panicked

Error message

lane proxy stdout logger panicked

What it means

run_lane_log_proxy spawns two threads (`stream_child_output`) to pipe the child's stdout and stderr into the lane NDJSON log, then joins them after the child exits. If the stdout logging thread panicked while holding its JoinHandle result, `join()` returns Err and this anyhow error is raised instead of the real cause. It signals a bug or I/O failure inside the logging worker, not a failure of the proxied child command itself.

Solutions

  1. Check the panic message in stderr/stdout of the proxy process (the panic prints where the thread died) and fix the underlying cause in `stream_child_output` — usually log-file I/O.
  2. Verify `log_path`'s parent directory exists and is writable for the whole lifetime of the child process.
  3. Check available disk space and inode/fd limits if the lane runs long or produces high output volume.
  4. Report/patch `stream_child_output` if it panics on recoverable I/O errors instead of returning them; the join Err payload is always the string panic message, so the real error is lost.
  5. Re-run the lane after fixing; the exit receipt will show LANE_PROXY_FAILURE_EXIT_CODE for this run.

Example fix

// before: panic inside the logging thread on I/O failure
let mut file = File::options().append(true).open(path).unwrap();
// after: propagate the error so join() yields Err(result), not a panic
let file = File::options().append(true).open(path)
    .with_context(|| format!("open log {}", path.display()))?;
writeln!(file, "{}", line).with_context(|| "append log line")?;
Defensive patterns

Strategy: try-catch

Validate before calling

let log_dir = log_path.parent().unwrap();
assert!(log_dir.exists(), "lane log dir missing: {}", log_dir.display());
let probe = std::fs::OpenOptions::new().append(true).create(true).open(&log_path);
assert!(probe.is_ok(), "lane log not writable: {:?}", probe.err());

Type guard

fn thread_result_ok<T>(r: std::thread::Result<T>) -> Option<T> { r.ok() }

Try / catch

match run_lane_log_proxy(spec) {
    Ok(code) if code != LANE_PROXY_FAILURE_EXIT_CODE => { /* normal */ }
    Ok(code) => eprintln!("lane proxy logging failed; inspect lane log for lane_proxy_error"),
    Err(e) if e.to_string().contains("panicked") => {
        eprintln!("stdout logger thread panicked: {e:#}");
        // check log dir permissions/disk, then retry
    }
    Err(e) => eprintln!("lane proxy failed: {e:#}"),
}

Prevention

When it happens

Trigger: Calling `run_lane_log_proxy` (or the `lane log-proxy` CLI path) where the stdout streaming thread panics mid-run — e.g. `stream_child_output` unwraps a failed append/write to `log_path`, or the log file handle becomes invalid (deleted directory, unwritable path, disk full) on a code path that panics instead of returning an error.

Common situations: Log directory removed while the lane child is running; permissions on the lane log changed mid-flight; a panic inside `stream_child_output`'s formatting/serialization on malformed child output; running many lanes concurrently and exhausting file descriptors so writes panic on unwrap.

Related errors


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

Appendix: source

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

        None => return fail(anyhow::anyhow!("lane child stderr was not piped"), 125),
    };
    let stdout_log = log_path.clone();
    let stderr_log = 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 = match child.wait() {
        Ok(status) => status,
        Err(error) => {
            return fail(
                anyhow::Error::from(error).context(format!("wait for lane child {command:?}")),
                LANE_PROXY_FAILURE_EXIT_CODE,
            );
        }
    };
    let stdout_result = stdout_thread
        .join()
        .map_err(|_| anyhow::anyhow!("lane proxy stdout logger panicked"))?;
    let stderr_result = stderr_thread
        .join()
        .map_err(|_| anyhow::anyhow!("lane proxy stderr logger panicked"))?;
    let mut exit_code = exit_status_code(status);
    if let Some(error) = stdout_result.err().or_else(|| stderr_result.err()) {
        append_proxy_failure(&log_path, &lane_id, &error)?;
        exit_code = LANE_PROXY_FAILURE_EXIT_CODE;
    }
    write_lane_exit_receipt(&receipt_path, &receipt_tmp_path, &lane_id, exit_code)?;
    Ok(exit_code)
}

fn read_lane_exit_receipt(log_path: &Path, lane_id: &str) -> Result<Option<LaneExitReceipt>> {
    let path = lane_exit_receipt_path(log_path);
    let metadata = match std::fs::metadata(&path) {
        Ok(metadata) => metadata,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(err) => return Err(err).with_context(|| format!("stat {}", path.display())),

View on GitHub (pinned to 73e0f67d83)