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
- Check which SQLite driver is compiled in; prefer one that implements RowsAffected correctly (e.g. mattn/go-sqlite3 or modernc.org/sqlite current versions).
- If the row count is only informational, degrade to logging a warning and treat the sweep as successful.
- Pin the driver version and re-run 'go mod tidy' after driver changes.
- 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
- Use a SQLite driver that supports RowsAffected (mattn/go-sqlite3, modernc.org/sqlite)
- Pin driver versions in go.mod and re-test after upgrades
- Only require the count when callers consume it; otherwise log-and-continue
- Add a driver smoke test that performs an UPDATE and asserts RowsAffected works
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
- keystore: sweep otp rows affected
- keystore: open database
- keystore: get agent key
- keystore: find agent by pubkey
- keystore: check agent registered
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)