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

  1. Read the wrapped driver error to distinguish lock contention from schema/storage problems.
  2. Schedule the sweep at a low-traffic interval or add retry-with-backoff around lock errors.
  3. Run migrations so agent_keys exists before the sweeper starts.
  4. Ensure the data directory is writable by the nhp-serverd process user.
  5. 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

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


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() - retentionSeconds

View on GitHub (pinned to 6e04ca5ff0)