OpenNHP/opennhp · error
keystore: sweep otp
Error message
keystore: sweep otp: %w
What it means
SweepStaleOTPs wraps failures of the DELETE that removes used/expired OTP rows older than the retention cutoff. The deletion predicate itself is fixed (created_at < cutoff AND (used=1 OR expires_at <= now)); only Exec-level DB failures produce this error.
Solutions
- Unwrap the error to identify the cause (no such table: otp_records / database is locked).
- Run migrations creating otp_records before starting the OTP sweeper.
- Enable WAL mode and busy_timeout to coexist with OTP traffic.
- Retry the sweep with backoff if the error is transient lock contention.
- Verify the process can write to the SQLite file and its containing directory.
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='otp_records'").Scan(&tables)
if tables == 0 { return errors.New("otp_records missing; migrate first") } Type guard
n, err := store.SweepStaleOTPs(retention)
if err != nil && isBusyOrLocked(err) {
time.Sleep(time.Second)
n, err = store.SweepStaleOTPs(retention)
} Try / catch
n, err := store.SweepStaleOTPs(86400)
if err != nil {
if strings.Contains(err.Error(), "no such table") {
return 0, errors.New("otp schema not migrated")
}
return 0, err
} Prevention
- Run OTP migrations before starting the retention sweeper
- Use WAL mode + busy_timeout to coexist with OTP write traffic
- Jitter sweep timing to avoid lock storms
- Watch disk space — DELETE failures can stem from full volumes
When it happens
Trigger: Calling SweepStaleOTPs(retentionSeconds) when otp_records does not exist (missing migration), the DB is locked by another writer, the handle is closed, or the filesystem is full/read-only.
Common situations: Retention sweeper running before the OTP schema migration; lock contention with OTP creation/verification traffic; container data mount being read-only.
Related errors
- keystore: insert otp
- keystore: query otp
- keystore: query pending otp
- keystore: get agent key
- keystore: find agent by pubkey
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/d8671fe8a0a1e1a8.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/keystore.go:562
// 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
res, err := s.db.Exec(
`DELETE FROM otp_records
WHERE created_at < ?
AND (used = 1 OR expires_at <= ?)`,
cutoff, time.Now().Unix(),
)
if err != nil {
return 0, fmt.Errorf("keystore: sweep otp: %w", err)
}
n, err := res.RowsAffected()
if err != nil {
return 0, fmt.Errorf("keystore: sweep otp rows affected: %w", err)
}
return n, nil
}
// ── Helpers ───────────────────────────────────────────────────────────────
func randomDigits(n int) (string, error) {
if n <= 0 {
return "", fmt.Errorf("invalid digit count: %d", n)
}
buf := make([]byte, n)
for i := range buf {
digit, err := rand.Int(rand.Reader, big.NewInt(10))View on GitHub (pinned to 6e04ca5ff0)