OpenNHP/opennhp · error
keystore: sweep expired
Error message
keystore: sweep expired: %w
What it means
SweepExpiredDeactivates wraps failures of the UPDATE that flips active=0 on rows whose expires_at has elapsed. This is the first of two failure points in the sweep; the second is RowsAffected (error 145). Any Exec failure — locked DB, missing table, read-only file — is wrapped with this message.
Solutions
- Read the wrapped driver error to distinguish lock contention from schema/storage problems.
- Schedule the sweep at a low-traffic interval or add retry-with-backoff around lock errors.
- Run migrations so agent_keys exists before the sweeper starts.
- Ensure the data directory is writable by the nhp-serverd process user.
- Use WAL journal mode so the UPDATE does not block on readers.
Example fix
null
Defensive patterns
Strategy: retry
Validate before calling
var tables int
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='agent_keys'").Scan(&tables)
if tables == 0 { return errors.New("agent_keys missing; migrate first") } Type guard
n, err := store.SweepExpiredDeactivates()
if err != nil {
if isBusyOrLocked(err) {
time.Sleep(backoff)
n, err = store.SweepExpiredDeactivates()
}
if err != nil { return err }
} Try / catch
for attempt := 0; attempt < 3; attempt++ {
n, err := store.SweepExpiredDeactivates()
if err == nil { return n, nil }
if !isLockError(err) { return 0, err }
time.Sleep(time.Duration(attempt+1) * time.Second)
}
return 0, errors.New("sweep: retries exhausted") Prevention
- Run the sweeper off-peak or with jittered intervals
- Enable WAL journal mode so UPDATE does not block on readers
- Ensure the data directory is writable (check container mounts)
- Confirm migrations ran before starting background sweepers
When it happens
Trigger: Calling SweepExpiredDeactivates() (typically from a periodic hygiene loop) when the database is locked by concurrent writers, agent_keys does not exist, the DB file is read-only, or the disk is full.
Common situations: Sweeper timer firing while a registration upsert holds the write lock; deploying a new server against an un-migrated DB; container with a read-only mount for the data directory.
Related errors
- keystore: get agent key
- keystore: find agent by pubkey
- keystore: check agent registered
- keystore: get agent key expiry
- keystore: sweep rows affected
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/a3fedacb031066a4.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/keystore.go:536
}
// SweepExpiredDeactivates flips active=0 for any row whose expires_at has
// elapsed. Returns the number of rows updated. NULL expires_at rows are
// never swept (they are configured to never expire). The result of
// FindAgentByPublicKey / IsAgentRegistered does not depend on this
// sweeper — those functions already filter on expires_at — so this
// method is purely a hygiene / index-utility measure.
func (s *AgentKeyStore) SweepExpiredDeactivates() (int64, error) {
res, err := s.db.Exec(
`UPDATE agent_keys
SET active = 0
WHERE active = 1
AND expires_at IS NOT NULL
AND expires_at <= ?`,
time.Now().Unix(),
)
if err != nil {
return 0, fmt.Errorf("keystore: sweep expired: %w", err)
}
n, err := res.RowsAffected()
if err != nil {
return 0, fmt.Errorf("keystore: sweep rows affected: %w", err)
}
return n, nil
}
// SweepStaleOTPs deletes OTP rows that are already used or expired and
// were created more than retentionSeconds ago. Returns the number of rows
// deleted. Unused, non-expired OTPs are never swept. Retention defaults
// to 86400s (24 hours) when passed a negative value. Pass 0 to delete all
// used or expired OTPs regardless of age.
func (s *AgentKeyStore) SweepStaleOTPs(retentionSeconds int64) (int64, error) {
if retentionSeconds < 0 {
retentionSeconds = 86400
}
cutoff := time.Now().Unix() - retentionSecondsView on GitHub (pinned to 6e04ca5ff0)