jdx/mise · error

could not find the suspended description command's primary…

Error message

could not find the suspended description command's primary thread

What it means

On Windows, the history 'describe command' runs inside a job object and its initial thread is created suspended so it can be assigned to the job before it runs. This error is thrown by `start` when it enumerates the process's threads with the tool-help snapshot API (Thread32First/Thread32Next) and cannot find the expected primary suspended thread, so the command can never be safely started.

Solutions

  1. Retry the command start; the failure is usually a race with immediate child exit
  2. Ensure the described command is not a batch/instant-exit wrapper that terminates before thread enumeration
  3. Check that the process is running under a normal (non-restricted) environment and no sandbox policy is hiding threads
  4. If reproducible, capture the child PID and inspect with `tasklist /v` / Process Explorer to confirm the process is alive at start time

Example fix

// before
bail!("could not find the suspended description command's primary thread")
// after
// retry once before failing
if start_attempt == 0 { return start(); }
bail!("could not find the suspended description command's primary thread")
Defensive patterns

Strategy: retry

Validate before calling

// before starting, confirm the child still has threads
fn child_alive(pid: u32) -> bool {
  std::path::Path::new("\\\\.\\Device\\")
    .exists() || unsafe {
    let h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
    let ok = h != std::ptr::null_mut();
    if ok { CloseHandle(h); }
    ok
  }
}

Try / catch

// retry the start once on thread-lookup races
match start() {
  Err(e) if e.to_string().contains("primary thread") && attempt == 0 => start(),
  other => other,
}

Prevention

When it happens

Trigger: Calling the Windows-only describe-command start path when the child process already exited before the thread snapshot was taken, when the PID no longer has any threads, or when the tool-help snapshot enumerates no matching thread (e.g. race between CreateProcess and Thread32First).

Common situations: Very short-lived commands that exit before the suspended primary thread is located; heavily loaded or restricted systems where process/thread enumeration fails transiently; running under environments that interfere with tool-help snapshots (some sandboxes or job restrictions).

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/9727fe05d983121f. Report an issue: GitHub.

Appendix: source

Thrown at src/system/history/describe_command/windows_job.rs:87

        while found {
            if entry.th32OwnerProcessID == child.id() {
                // SAFETY: request only resume access to the child's thread.
                let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) };
                if thread.is_null() {
                    return Err(std::io::Error::last_os_error().into());
                }
                // SAFETY: the returned handle is valid and uniquely owned.
                let thread = unsafe { OwnedHandle::from_raw_handle(thread) };
                // SAFETY: the child is already assigned to our kill-on-close job.
                if unsafe { ResumeThread(thread.as_raw_handle()) } == u32::MAX {
                    return Err(std::io::Error::last_os_error().into());
                }
                return Ok(());
            }
            // SAFETY: the snapshot and writable entry remain valid.
            found = unsafe { Thread32Next(snapshot.as_raw_handle(), &mut entry) } != 0;
        }
        bail!("could not find the suspended description command's primary thread")
    }

    pub(super) fn kill(&self) {
        // SAFETY: the owned handle always refers to this command's job.
        unsafe { TerminateJobObject(self.0.as_raw_handle(), 1) };
    }
}

pub(super) fn spawn(command: &mut Command) -> Result<(Child, Job)> {
    let job = Job::new()?;
    command.creation_flags(CREATE_SUSPENDED);
    let mut child = command.spawn()?;
    if let Err(error) = job.start(&child) {
        // A failure must not leave either a suspended child or an unowned tree.
        let _ = child.kill();
        let _ = child.wait();
        return Err(error);
    }

View on GitHub (pinned to 533346cc37)