OpenNHP/opennhp · error

keystore: insert agent key

Error message

keystore: insert agent key: %w

What it means

RegisterAgentKey performs an INSERT ... ON CONFLICT DO UPDATE (upsert) into agent_keys; failure is wrapped as 'keystore: insert agent key'. Causes include constraint violations, SQLITE_BUSY, disk I/O errors, or a driver not supporting the upsert syntax.

Solutions

  1. Check the wrapped error: 'syntax error near ON' means the driver's SQLite is too old — upgrade modernc.org/sqlite or mattn/go-sqlite3.
  2. Free disk space / verify volume writability (WAL needs db, -wal, -shm writes).
  3. Reduce cross-process write contention or serialize registrations.
  4. Re-run the registration; the upsert is idempotent for the same key.
Defensive patterns

Strategy: retry

Try / catch

err := store.RegisterAgentKey(u, d, pk, cs, ttl)
if err != nil && strings.Contains(err.Error(), "insert agent key") {
    if strings.Contains(err.Error(), "syntax error") {
        log.Fatal("sqlite driver too old for ON CONFLICT upsert; upgrade driver")
    }
    // bounded retry for transient BUSY
}

Prevention

When it happens

Trigger: Upsert Exec fails: UNIQUE constraint conflict beyond the handled idempotent path, WAL checkpoint I/O failure on a full disk, or old SQLite versions (<3.24) lacking ON CONFLICT DO UPDATE support.

Common situations: Embedded/older SQLite builds in the driver rejecting the upsert syntax ('near ON: syntax error'), disk-full containers, concurrent writers across processes.

Related errors


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

Appendix: source

Thrown at endpoints/server/keystore.go:416

		expiresAt = sql.NullInt64{Int64: now + ttlSeconds, Valid: true}
	}

	// Upsert: insert or update on (usr_id, dev_id) conflict. Both fresh
	// inserts and key rotations (including cipher scheme switches) reset
	// the clock.
	_, err = s.db.Exec(
		`INSERT INTO agent_keys (usr_id, dev_id, public_key, cipher, created_at, expires_at, active)
		 VALUES (?, ?, ?, ?, ?, ?, 1)
		 ON CONFLICT(usr_id, dev_id) DO UPDATE SET
		   public_key = excluded.public_key,
		   cipher     = excluded.cipher,
		   created_at = excluded.created_at,
		   expires_at = excluded.expires_at,
		   active     = 1`,
		userId, deviceId, pubKey, cipherScheme, now, expiresAt,
	)
	if err != nil {
		return fmt.Errorf("keystore: insert agent key: %w", err)
	}

	log.Info("keystore: agent key registered for user=%s device=%s cipher=%d ttl=%ds", userId, deviceId, cipherScheme, ttlSeconds)
	return nil
}

// GetAgentKey returns the public key for a given user+device, or nil if
// not found OR if the row is past its expires_at. Expired rows are
// indistinguishable from never-registered ones to all callers.
func (s *AgentKeyStore) GetAgentKey(userId, deviceId string) (*AgentKeyRecord, error) {
	rec := &AgentKeyRecord{}
	var expiresAt sql.NullInt64
	var active int
	err := s.db.QueryRow(
		`SELECT usr_id, dev_id, public_key, cipher, created_at, expires_at, active
		 FROM agent_keys
		 WHERE usr_id = ? AND dev_id = ? AND active = 1
		   AND (expires_at IS NULL OR expires_at > ?)`,

View on GitHub (pinned to 6e04ca5ff0)