jdx/mise · error

a process it started kept its output open

Error message

a process it started kept its output open

What it means

Thrown by `run_with_limits` in describe_command.rs when the command's output channel produces nothing within `output_grace` after the shell exits. This indicates a descendant process the shell spawned kept the inherited stdout/stderr pipe open, so the collector cannot distinguish 'shell finished' from 'shell still writing'. The library intentionally refuses to wait forever on orphaned descendants and kills the remaining active process instead.

Source

Thrown at src/system/history/describe_command.rs:175

    let status = loop {
        if let Some(status) = child.try_wait()? {
            break status;
        }
        if started.elapsed() >= timeout {
            // the shell and whatever it started; the reader thread ends
            // with the last writer of the pipe, so it is not waited for
            active.0.kill();
            let _ = child.kill();
            let _ = child.wait();
            bail!("took longer than {}s", timeout.as_secs());
        }
        std::thread::sleep(Duration::from_millis(100));
    };
    // a descendant that outlived the shell and kept the pipe is not the
    // shell's answer: the output is waited for a moment, not forever
    let Ok(output) = receiver.recv_timeout(output_grace) else {
        active.0.kill();
        bail!("a process it started kept its output open");
    };
    if output.len() > DIFF_LIMIT {
        bail!("description output exceeded {} bytes", DIFF_LIMIT);
    }
    if !status.success() {
        bail!("exited with {status}");
    }
    let Some(line) = first_line(&output) else {
        return Ok(None);
    };
    annotate(
        store,
        entry,
        Annotation {
            description: Some(line.clone()),
            description_source: Some(DescriptionSource::Command),
            labels: None,
            updated_at: store::now_rfc3339(),

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Rewrite the command so background children do not inherit stdout/stderr: redirect them to a file or /dev/null (e.g. `my-daemon &>/dev/null &`).
  2. Use `setsid`/`disown` with full redirection so the descendant detaches from the inherited pipe.
  3. Increase the grace window if the command legitimately produces output slowly, via the run_with_limits configuration.
  4. Ensure the description command itself exits promptly instead of blocking on children it spawns.

Example fix

// before
logging-agent --follow &
echo done
// after
logging-agent --follow >/dev/null 2>&1 &
disown
echo done
Defensive patterns

Strategy: try-catch

Validate before calling

// shell script audit: no background jobs inheriting stdout
// grep -nE '&([^>]|$)' my-hook.sh  — ensure each is redirected

Try / catch

// match run_with_limits(...) {
//     Err(e) if e.message().contains("kept its output open") => {
//         // command spawned a daemon; treat as inconclusive, not fatal
//     }
//     other => other?,
// }

Prevention

When it happens

Trigger: Running a description command whose shell spawns a background child (daemon, `tail -f`, `nohup ... &`, detached process) that inherits the stdout pipe and outlives the shell; `receiver.recv_timeout(output_grace)` then times out even though the shell itself exited.

Common situations: A dotfile hook or annotation command that starts a background agent or logging process; scripts that leave `&` background jobs attached to the terminal; watchdog processes spawned by shell profile files executed during command startup.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/4ad3fcb36fc50b4d. Report an issue: GitHub.