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 "", errProcessExitedView on GitHub (pinned to 71377f2769)
Solutions
- Call Handle.Close and re-run procid.Capture/Open to obtain a fresh, consistent handle+token pair
- Use procid.IsProcessGone on the error from Signal to distinguish exited targets (errProcessExited is returned separately and Signal treats it as success)
- Shorten the lifetime of held handles; open, signal, close instead of holding indefinitely
- 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
- Prefer open→signal→close over holding handles for long periods
- Re-capture tokens whenever the target may have restarted
- Note Signal returns nil for already-exited targets; only token mismatch indicates stale identity
- Never mutate Handle fields outside the library
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
- procid: process %d does not match token
- finding process %d: %w
- finding process %d: %w
- procid: process %d no longer matches token
- procid: terminate process: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/c614fabc44763969.
Report an issue: GitHub.