jdx/mise · error

a process it started kept its output open

Error message

a process it started kept its output open

What it means

After the shell exits, run_with_limits waits only output_grace for the output pipe to close. A descendant process that outlived the shell and still holds the stdout pipe is not considered the shell's answer; mise kills it and bails with 'a process it started kept its output open'. This guards against background children hijacking or indefinitely extending the command's output.

Solutions

  1. Redirect background processes' stdout/stderr away from the describe command's output (e.g. `> /dev/null 2>&1` or to a log file)
  2. Ensure the describe command waits for or terminates its children before exiting
  3. If a legitimately slow writer is expected, raise the output grace period in the history settings

Example fix

# before: child keeps the pipe open
some-daemon &
echo done

# after: detach the child's output
some-daemon > /dev/null 2>&1 &
echo done
Defensive patterns

Strategy: validation

Validate before calling

# check the script does not leave stdout-holding children behind
describe-command; sleep 2; ls /proc/*/fd 2>/dev/null | grep -c pipe || true  # or run: timeout 5 describe-command >/dev/null && pgrep -f child-daemon

Prevention

When it happens

Trigger: The describe command's shell exits but spawns a background/detached child that inherits stdout and keeps it open longer than the output grace period (e.g. `cmd &`, daemons, watchers).

Common situations: Describe scripts that background long-running helpers (log tailers, dev servers); `nohup`-style launches inside the describe command; shells leaving stray children on abnormal exit.

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@533346cc37 (2026-09-17). Data as JSON: /api/errors/4ad3fcb36fc50b4d. Report an issue: GitHub.

Appendix: source

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

    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 533346cc37)