gastownhall/beads · error

procid: process no longer matches token

Error message

procid: process no longer matches token

What it means

Handle.verify (used by Signal before terminating) re-mints the token from the held process handle and finds it differs from the token captured at Open time. The library refuses to signal: the handle is believed to refer to a process instance other than the one originally opened, i.e. suspected PID reuse or stale state.

Source

Thrown at internal/procid/procid_windows.go:112

func (h *Handle) Close() error {
	if h.process == 0 {
		return nil
	}
	err := windows.CloseHandle(h.process)
	h.process = 0
	if err != nil {
		return fmt.Errorf("procid: close process handle: %w", err)
	}
	return nil
}

func (h *Handle) verify() error {
	current, err := tokenForProcess(h.process)
	if err != nil {
		return err
	}
	if current != h.token {
		return fmt.Errorf("procid: process no longer matches token")
	}
	return nil
}

func openProcess(pid int, access uint32) (windows.Handle, error) {
	return windows.OpenProcess(access, false, uint32(pid))
}

func tokenForProcess(process windows.Handle) (Token, error) {
	// An open handle keeps a terminated process's PID resolvable and its
	// creation time readable, so check liveness explicitly before minting a
	// token for it.
	var code uint32
	if err := windows.GetExitCodeProcess(process, &code); err != nil {
		return "", fmt.Errorf("procid: get process exit code: %w", err)
	}
	if code != stillActive {
		return "", errProcessExited

View on GitHub (pinned to 71377f2769)

Solutions

  1. Call Handle.Close and re-run procid.Capture/Open to obtain a fresh, consistent handle+token pair
  2. Use procid.IsProcessGone on the error from Signal to distinguish exited targets (errProcessExited is returned separately and Signal treats it as success)
  3. Shorten the lifetime of held handles; open, signal, close instead of holding indefinitely
  4. If you see this repeatedly, verify tokens aren't being persisted/reloaded across process restarts

Example fix

// before
if err := h.Kill(); err != nil { return err }
// after
if err := h.Kill(); err != nil {
    _ = h.Close()
    return fmt.Errorf("stale handle for pid %d, re-open required: %w", pid, err)
}
Defensive patterns

Strategy: fallback

Validate before calling

// Before signaling a long-held handle, re-verify the token still matches
// (verify() runs inside Signal; pre-check externally with Verify)
ok, err := procid.Verify(pid, tok)
if err != nil || !ok {
    return nil // stale; do not signal
}

Type guard

func handleIsCurrent(h *procid.Handle, pid int, tok procid.Token) bool {
    ok, err := procid.Verify(pid, tok)
    return err == nil && ok
}

Try / catch

if err := h.Kill(); err != nil {
    _ = h.Close()
    // rebuild handle+token pair and retry once
    newTok, cerr := procid.Capture(pid)
    if cerr != nil { return cerr }
    nh, oerr := procid.Open(pid, newTok)
    if oerr != nil { return oerr }
    return nh.Kill()
}

Prevention

When it happens

Trigger: Calling Handle.Signal/Kill after the target process exited and its PID was recycled while some other handle kept the object alive; external code replaced or corrupted the handle's token field.

Common situations: Holding a Handle for a long time across process restarts; the process exited but a third party (Task Manager, antivirus) keeps the process object alive so the handle still resolves.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/c614fabc44763969. Report an issue: GitHub.