gastownhall/beads · error

procid: process %d still matches token after fatal signal an

Error message

procid: process %d still matches token after fatal signal and %s re-check

What it means

After sending a fatal signal (SIGKILL/SIGTERM) on darwin, Handle.Signal polls Verify for up to fallbackSignalConfirmTimeout (2s) to confirm the process actually died. If the process still matches the token after the timeout, the library reports that the kill did not take effect within the confirmation window.

Source

Thrown at internal/procid/procid_darwin.go:97

	}
	if !match {
		return fmt.Errorf("procid: process %d no longer matches token", h.pid)
	}
	return nil
}

func (h *Handle) confirmFatalSignal() error {
	deadline := time.Now().Add(fallbackSignalConfirmTimeout)
	for {
		match, err := Verify(h.pid, h.token)
		if err != nil {
			return err
		}
		if !match {
			return nil
		}
		if time.Now().After(deadline) {
			return fmt.Errorf(
				"procid: process %d still matches token after fatal signal and %s re-check",
				h.pid,
				fallbackSignalConfirmTimeout,
			)
		}
		time.Sleep(10 * time.Millisecond)
	}
}

func isFatalSignal(sig syscall.Signal) bool {
	return sig == syscall.SIGKILL || sig == syscall.SIGTERM
}

func (h *Handle) Kill() error { return h.Signal(syscall.SIGKILL) }

func (h *Handle) Close() error { return nil }

// IsProcessGone reports whether err means the referenced process no longer

View on GitHub (pinned to 71377f2769)

Solutions

  1. Send SIGKILL instead of SIGTERM if the target may trap SIGTERM (SIGKILL cannot be handled).
  2. Wait and re-check: poll Verify yourself after returning, since the process may exit just after the 2s window.
  3. Increase tolerance in your caller: retry the kill after this error before treating it as a hard failure.
  4. Investigate the target for stuck states (D-state, blocked handlers) if it survives SIGKILL for more than 2 seconds.

Example fix

// before
if err := handle.Kill(); err != nil {
    return fmt.Errorf("hard failure: %w", err)
}
// after
if err := handle.Kill(); err != nil {
    if strings.Contains(err.Error(), "still matches token") {
        time.Sleep(500 * time.Millisecond)
        _ = handle.Kill() // SIGKILL retry; then treat as gone if token changed
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// No pre-call validation possible; target may trap SIGTERM. Prefer SIGKILL if survival is unacceptable:
// h.Kill() instead of h.Signal(syscall.SIGTERM)

Try / catch

if err := h.Kill(); err != nil {
    if strings.Contains(err.Error(), "still matches token") {
        time.Sleep(2 * time.Second)
        return h.Kill() // one retry; process may have needed longer to exit
    }
    return err
}

Prevention

When it happens

Trigger: Handle.Signal(SIGKILL)/Kill() (or SIGTERM) where the process survives the full 2-second confirmation loop while still matching its token — e.g. SIGTERM trapped/ignored by the target, or the system is heavily loaded so exit is delayed.

Common situations: Applications installing a SIGTERM handler that blocks shutdown; processes stuck in uninterruptible work that delay reaping; heavily loaded CI machines; sending SIGTERM (treated as fatal here) to a process that handles it gracefully and takes longer than 2s to exit.

Related errors


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