Hmbown/CodeWhale · error

Job merges stderr into stdout

Error message

Job {task_id} merges stderr into stdout

What it means

Shell output chunking invariant: the job was started with stderr merged into stdout, so reads must be served from the single merged tail buffer; this error fires when a read path assumes separate streams and would mis-slice the merged buffer.

Solutions

  1. Read stdout and stderr as separate streams/channels instead of requesting merged output for this job.
  2. Re-run the job without the merge-streams option if separate capture is required.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at crates/tui/src/tools/shell.rs:3251 when the library encounters an invalid state.

Common situations: See trigger scenarios.


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

Appendix: source

Thrown at crates/tui/src/tools/shell.rs:3251

            let tail_start = total.saturating_sub(tail.len());
            let offset = cursor.max(tail_start).min(total);
            let next_offset = offset.saturating_add(max_bytes).min(total);
            return Ok(ShellOutputChunk {
                offset,
                bytes: tail.as_bytes()[offset - tail_start..next_offset - tail_start].to_vec(),
                next_offset,
                total,
                dropped: tail_start,
                status: snapshot.status.clone(),
                exit_code: snapshot.exit_code,
            });
        };
        let buffer = match stream {
            ShellOutputStream::Stdout => shell.stdout_buffer.clone(),
            ShellOutputStream::Stderr => shell
                .stderr_buffer
                .clone()
                .ok_or_else(|| anyhow!("Job {task_id} merges stderr into stdout"))?,
        };

        let wait_deadline = (wait_ms > 0 && shell.status == ShellStatus::Running)
            .then(|| Instant::now() + Duration::from_millis(wait_ms.clamp(50, 30_000)));
        loop {
            shell.poll();
            let total = buffer.lock().map(|guard| guard.total_len()).unwrap_or(0);
            let done_waiting = total > cursor
                || shell.status != ShellStatus::Running
                || wait_deadline.is_none_or(|deadline| Instant::now() >= deadline);
            if done_waiting {
                break;
            }
            std::thread::sleep(Duration::from_millis(50));
        }

        let (bytes, offset, next_offset, total, dropped) = {
            let guard = buffer.lock().unwrap_or_else(|e| e.into_inner());

View on GitHub (pinned to 73e0f67d83)