github/copilot-sdk · error

CLI initial thread was not found

Error message

CLI initial thread was not found

What it means

resume_initial_thread enumerates threads of the (suspended) CLI process via the Win32 tool-help snapshot to find and resume its initial thread. If the snapshot loop completes without matching a thread belonging to the process, it raises NotFound — the process likely exited or has no enumerable threads.

Solutions

  1. Confirm the CLI process is still alive (check exit code) before calling spawn-with-job helpers
  2. Retry the spawn; an instant-exit race is often transient (e.g. during system churn)
  3. Investigate why the CLI dies at startup (missing DLLs, bad config, AV)
  4. Verify snapshot creation succeeded and the PID passed is the CLI's own PID

Example fix

// before
resume_initial_thread(pid)?; // NotFound if process already gone
// after
if !is_process_alive(pid) {
    return Err(io::Error::new(io::ErrorKind::NotFound, "CLI exited before thread enumeration"));
}
resume_initial_thread(pid)?;
Defensive patterns

Strategy: retry

Try / catch

// Rust
match spawn_with_resume(cmd) {
    Err(e) if e.kind() == io::ErrorKind::NotFound && attempts < 2 => retry_after_delay(),
    r => r?,
}

Prevention

When it happens

Trigger: Thread32First/Thread32Next finds no entry with th32OwnerProcessID equal to the CLI PID — the process exited before enumeration, or thread enumeration was denied/failed on a hardened system.

Common situations: CLI crashing instantly on Windows (see exit reason in logs); PID reuse or race with process death; restrictive security policy blocking tool-help snapshots.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/07a676c827f80166. Report an issue: GitHub.

Appendix: source

Thrown at rust/src/process_tree.rs:179

            if entry.th32OwnerProcessID == pid {
                // SAFETY: the thread id came from the live system snapshot.
                let raw_thread =
                    unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) };
                if raw_thread.is_null() {
                    return Err(io::Error::last_os_error());
                }
                let thread = OwnedHandle(raw_thread);
                // SAFETY: this is the root's suspended initial thread.
                if unsafe { ResumeThread(thread.0) } == u32::MAX {
                    return Err(io::Error::last_os_error());
                }
                return Ok(());
            }
            // SAFETY: same valid snapshot and initialized entry as above.
            found = unsafe { Thread32Next(snapshot.0, &mut entry) } != 0;
        }

        Err(io::Error::new(
            io::ErrorKind::NotFound,
            "CLI initial thread was not found",
        ))
    }

    impl Tree {
        pub(super) fn terminate(&self) -> io::Result<()> {
            // SAFETY: the handle is a live Job Object owned by this value.
            if unsafe { TerminateJobObject(self.job.0, 1) } != 0 {
                Ok(())
            } else {
                Err(io::Error::last_os_error())
            }
        }
    }
}

View on GitHub (pinned to cd8cf15dc3)