OpenNHP/opennhp · warning

keystore: sweep rows affected

Error message

keystore: sweep rows affected: %w

What it means

SweepExpiredDeactivates wraps the failure of res.RowsAffected() after the UPDATE succeeded. Some drivers cannot report affected-row counts (e.g. certain SQLite builds or when the statement was prepared through an interface that discards metadata); this error surfaces that limitation.

Solutions

  1. Check which SQLite driver is compiled in; prefer one that implements RowsAffected correctly (e.g. mattn/go-sqlite3 or modernc.org/sqlite current versions).
  2. If the row count is only informational, degrade to logging a warning and treat the sweep as successful.
  3. Pin the driver version and re-run 'go mod tidy' after driver changes.
  4. Count affected rows separately with a SELECT COUNT(*) of rows still active and expired if exact numbers are required.

Example fix

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

Strategy: fallback

Validate before calling

null

Type guard

n, err := res.RowsAffected()
if err != nil {
    // treat as unknown count; verify via follow-up SELECT COUNT(*)
    db.QueryRow("SELECT COUNT(*) FROM agent_keys WHERE active=1 AND expires_at IS NOT NULL AND expires_at <= ?", time.Now().Unix()).Scan(&remaining)
}

Try / catch

n, err := res.RowsAffected()
if err != nil {
    log.Warnf("rows affected unavailable: %v", err)
    n = -1 // sweep already applied; do not fail
}

Prevention

When it happens

Trigger: Calling SweepExpiredDeactivates() against a driver/driver-version whose Result does not support RowsAffected (returns an error), after the deactivation UPDATE itself already ran.

Common situations: Switching SQLite drivers (mattn/go-sqlite3 vs modernc.org/sqlite) or upgrading versions changes RowsAffected behavior; using a proxy/ConnectionHook that returns a driver.Result without row counts.

Related errors


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

Appendix: source

Thrown at endpoints/server/keystore.go:540

// 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
	res, err := s.db.Exec(
		`DELETE FROM otp_records
		 WHERE created_at < ?
		   AND (used = 1 OR expires_at <= ?)`,

View on GitHub (pinned to 6e04ca5ff0)