gastownhall/beads · error
proxy.ForceStopUnverified: record %s changed after pid %d wa
Error message
proxy.ForceStopUnverified: record %s changed after pid %d was stopped; refusing to quarantine the replacement
What it means
Thrown by quarantineForceStopRecord in internal/storage/dbproxy/proxy/force_stop.go when, after a force-stop killed the pid named in a record, a re-read of the record on disk differs from the in-memory snapshot. This means another process replaced or modified the record after the pid died, so quarantining it would archive the wrong (replacement) data. The library refuses to move it to quarantine to avoid destroying a legitimate new record.
Source
Thrown at internal/storage/dbproxy/proxy/force_stop.go:317
time.Sleep(shutdownConfirmPoll)
}
}
func quarantineForceStopRecord(
rootDir string,
pidName string,
record *pidfile.PidFile,
report *ForceStopReport,
) error {
current, err := pidfile.Read(rootDir, pidName)
if err != nil {
return fmt.Errorf("proxy.ForceStopUnverified: re-read %s: %w", report.RecordPath, err)
}
if current == nil {
return nil
}
if *current != *record {
return fmt.Errorf(
"proxy.ForceStopUnverified: record %s changed after pid %d was stopped; refusing to quarantine the replacement",
report.RecordPath,
record.Pid,
)
}
target, err := quarantineRecord(rootDir, pidName, time.Now())
if err != nil {
return fmt.Errorf("proxy.ForceStopUnverified: quarantine %s: %w", report.RecordPath, err)
}
report.QuarantinedPath = target
return nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Re-run the force-stop so it re-reads the current record and targets the correct pid
- Check who/what modified the record (concurrent bd process, restart supervisor) and serialize force-stops
- If the replacement record is genuinely unwanted, delete it explicitly rather than forcing quarantine
Example fix
// before: racing force-stop
forceStopRecord(rootDir, staleRecord)
// after: re-read then stop under lock
rec, err := readPidRecord(rootDir)
if err != nil { return err }
forceStopRecordLocked(rootDir, rec) Defensive patterns
Strategy: validation
Validate before calling
rec, err := readPidRecord(rootDir)
if err != nil { return err }
// re-read right before force-stop to confirm unchanged
again, err := readPidRecord(rootDir)
if err != nil || *again != *rec { return fmt.Errorf("record changed; retry") } Type guard
func recordStale(a, b *PidRecord) bool { return a == nil || b == nil || *a != *b } Try / catch
if err := forceStopRecord(rootDir, rec); err != nil {
if strings.Contains(err.Error(), "changed after pid") { /* retry with fresh read */ }
return err
} Prevention
- Serialize force-stop operations via a single control path
- Re-read the pid record immediately before stopping
- Check for restart supervisors before force-stopping
When it happens
Trigger: Calling ForceStopUnverified (via forceStopRecord / forceStopRecordLocked) when a concurrent writer rewrites the pid record between the initial read/stop and the post-stop re-read; *current != *record.
Common situations: Two operators or automation scripts run force-stop concurrently; a supervisor restarts the daemon and the new process rewrites its own record while an old force-stop is in flight; stale tooling operating on a recycled pid.
Related errors
- procid: malformed proc stat: missing comm terminator
- procid: malformed proc stat: missing starttime
- target HEAD disagrees with git worktree registry (registry %
- failed to pin target git directory identity: %w
- target is no longer registered at %s
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5bc4ce8c71f1fa34.
Report an issue: GitHub.