gastownhall/beads · warning

ErrUnverifiableProcess

ErrUnverifiableProcess

Error message

proxy process identity is unverifiable

What it means

ErrUnverifiableProcess marks lifecycle operations (stop/force-stop) that refuse to signal a process because its workspace-scoped identity (pidfile PID + birth token) could not be verified. It is wrapped by unverifiableLifecycleError which supplies the detail message; callers use errors.Is to offer a narrower recovery path without misclassifying unrelated shutdown failures.

Source

Thrown at internal/storage/dbproxy/proxy/endpoint.go:98

	quarantineRetention = 30 * 24 * time.Hour

	spawnMarkerFileName = "proxy.spawn"
	stopEpochFileName   = "proxy.stop-epoch"
)

var ResolveExecutable = os.Executable

var (
	verifyProcessIdentity = procid.Verify
	resolveRootIdentity   = identity.RootID
	readControlSecret     = identity.ReadSecret
)

// ErrUnverifiableProcess marks lifecycle operations that refuse to signal a
// process because its workspace-scoped identity cannot be verified. Callers
// may use errors.Is to offer a narrower recovery path without treating
// unrelated shutdown failures as identity failures.
var ErrUnverifiableProcess = errors.New("proxy process identity is unverifiable")

type unverifiableLifecycleError struct {
	message string
}

func (e *unverifiableLifecycleError) Error() string {
	return e.message
}

func (e *unverifiableLifecycleError) Unwrap() error {
	return ErrUnverifiableProcess
}

var stopEpochSequence atomic.Uint64

// beforeProxyChildStart is a deterministic test hook for the otherwise tiny
// release-lock-before-exec scheduling window. Production leaves it as a no-op.
var beforeProxyChildStart = func() {}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat it as a safety refusal: use errors.Is(err, ErrUnverifiableProcess) and take the explicit force-stop/unverified recovery path the API exposes
  2. Remove the stale pidfile so the next start does a clean spawn instead of trying to stop an unverified process
  3. Investigate what process holds the port before force-stopping; do not blindly kill

Example fix

// before
if err := ep.Stop(ctx); err != nil { return err }
// after
if err := ep.Stop(ctx); err != nil {
    if errors.Is(err, endpoint.ErrUnverifiableProcess) {
        return ep.ForceStopUnverified(ctx) // explicit user-approved path
    }
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

if !pidfileMatchesProcess(pf) { /* skip or quarantine instead of signaling */ }

Type guard

func isUnverifiable(err error) bool { return errors.Is(err, endpoint.ErrUnverifiableProcess) }

Try / catch

if err := ep.Stop(ctx); err != nil {
    if errors.Is(err, endpoint.ErrUnverifiableProcess) {
        return handleUnverified(ep) // explicit recovery path
    }
    return err
}

Prevention

When it happens

Trigger: Attempting to stop a proxy whose live process cannot be matched against the pidfile's birth token; shutdown hitting a foreign or recycled-PID process; CanForceStopUnverified gating refusals for unverified records.

Common situations: PID reuse after a crash so the pidfile PID now belongs to an unrelated process; shared workspace directory with another checkout's proxy; stale pidfile pointing at a foreign listener.

Related errors


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