OpenNHP/opennhp · warning

keystore: sweep otp rows affected

Error message

keystore: sweep otp rows affected: %w

What it means

SweepStaleOTPs wraps the failure of res.RowsAffected() after the OTP DELETE succeeded. Same driver-level limitation as the agent_keys sweep: some drivers/Result implementations cannot report the number of deleted rows.

Solutions

  1. Verify the SQLite driver implements RowsAffected; upgrade or switch drivers if it does not.
  2. If the deleted count is only for logging, log the error and continue instead of failing the sweep.
  3. Pin known-good driver versions in go.mod to avoid behavior changes on upgrade.
  4. If an exact count matters, wrap the DELETE in a transaction and pre-count matching rows.

Example fix

// before
n, err := res.RowsAffected()
if err != nil {
    return 0, fmt.Errorf("keystore: sweep otp rows affected: %w", err)
}
// after
n, err := res.RowsAffected()
if err != nil {
    log.Warnf("keystore: otp sweep completed, count unavailable: %v", err)
    n = -1
}
Defensive patterns

Strategy: fallback

Validate before calling

null

Type guard

n, err := res.RowsAffected()
if err != nil {
    db.QueryRow("SELECT COUNT(*) FROM otp_records WHERE created_at < ? AND (used=1 OR expires_at <= ?)", cutoff, time.Now().Unix()).Scan(&remaining)
}

Try / catch

n, err := res.RowsAffected()
if err != nil {
    log.Warnf("otp sweep count unavailable: %v", err)
    n = -1
}

Prevention

When it happens

Trigger: Calling SweepStaleOTPs against a driver whose Result lacks RowsAffected support, even though the DELETE executed.

Common situations: Driver swap or version upgrade in go.mod; custom driver wrappers that drop row-count metadata; embedded SQLite variants with limited metadata support.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/1132661b8f0d28f7. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/server/keystore.go:566

// 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))
		if err != nil {
			return "", err
		}
		buf[i] = byte('0') + byte(digit.Int64())

View on GitHub (pinned to 6e04ca5ff0)