Hmbown/CodeWhale · error

lane proxy stderr logger panicked

Error message

lane proxy stderr logger panicked

What it means

Identical sibling of the stdout variant: after the proxied child exits, `run_lane_log_proxy` joins the stderr streaming thread; if that thread panicked, `join()` returns Err and this error is raised. It means the thread responsible for capturing the child's stderr into the lane log died unexpectedly, so stderr output may be missing from the log and the lane is marked with the proxy failure exit code.

Solutions

  1. Inspect the panic backtrace printed by the dying thread to find the real failure inside `stream_child_output`.
  2. Ensure `log_path` remains present and writable for the full child lifetime; restore permissions or recreate the parent directory.
  3. Free disk space / raise fd limits if the run exhausted them.
  4. Patch `stream_child_output` to return io::Errors instead of unwrapping, so join() yields the real error and the log gets a `lane_proxy_error` event with the actual cause.
  5. Re-run the lane; note the exit receipt records LANE_PROXY_FAILURE_EXIT_CODE even though the child itself may have succeeded.

Example fix

// before
let written = file.write_all(line.as_bytes()).unwrap();
// after
file.write_all(line.as_bytes())
    .with_context(|| "lane proxy stderr log append")?;
Defensive patterns

Strategy: try-catch

Validate before calling

let meta = std::fs::metadata(log_path.parent().unwrap())?;
let writable = !meta.permissions().readonly();
assert!(writable, "lane log parent is read-only");

Type guard

fn logger_thread_survived<T>(r: std::thread::Result<T>) -> bool { r.is_ok() }

Try / catch

match run_lane_log_proxy(spec) {
    Err(e) if e.to_string().contains("stderr logger panicked") => {
        eprintln!("stderr logger thread died: {e:#}; check log file state and re-run");
    }
    Err(e) => eprintln!("lane proxy failed: {e:#}"),
    Ok(code) => { /* use exit receipt */ }
}

Prevention

When it happens

Trigger: Calling `run_lane_log_proxy` where the stderr thread (`stream_child_output(stderr, stderr_log, "stderr")`) panics — typically an unwrap on a failed append to the shared `log_path` or a panic while serializing an NDJSON event.

Common situations: Same as stdout variant: log path deleted/unwritable mid-run, disk full, fd exhaustion, a bug in `stream_child_output` that panics on unexpected input. Especially visible when the child writes large or malformed output to stderr.

Related errors


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

Appendix: source

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

    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())),
    };
    if metadata.len() > MAX_EXIT_RECEIPT_BYTES {
        bail!(

View on GitHub (pinned to 73e0f67d83)