Skip to content

fix(state): harden state.db against corruption — write lock, WAL protections, reconnect self-heal - #82280

Closed
shali10 wants to merge 3 commits into
NousResearch:mainfrom
shali10:main
Closed

fix(state): harden state.db against corruption — write lock, WAL protections, reconnect self-heal#82280
shali10 wants to merge 3 commits into
NousResearch:mainfrom
shali10:main

Conversation

@shali10

@shali10 shali10 commented Aug 9, 2026

Copy link
Copy Markdown

Summary

Production hardening for the canonical session store (state.db), fixing the corruption classes observed on a live deployment (v0.20.0):

  • Runtime connection corruption (file is not a database): a sibling process (forked curator agent, external repair pass) replacing/truncating the backing file breaks the live connection. Fix: bounded one-shot reconnect self-heal on the write path.
  • Mixed journal-mode corruption: journal_mode=DELETE was being applied while sibling connections held WAL open, unlinking live -wal/-shm files. Fix: refuse to downgrade when on-disk mode is unknown (existing is None → conservative raise; with require_wal → clear WalUnsupportedError).
  • No write-level serialization: concurrent writers could interleave. Fix: state_db_write_lock (process-local write lock).
  • NUL-token guard: decoded binary bytes tokenized into bogus script paths crashed Path.expanduser() when HOME is unset.
  • close() checkpoint safety: close() must never issue TRUNCATE/RESTART checkpoint (only PASSIVE), matching the WAL-reset vulnerability fix.

Also: PRAGMA journal_mode=WAL reads now retry transient disk i/o error (ZFS/APFS-CoW), and terminal_tool blocks deleting/moving the live state.db from inside the gateway process.

Commits (8)

  1. chore: snapshot local patches before upstream rebase — baseline snapshot of local patches (state.db protection, busy_timeout, qqbot/telegram fixes)
  2. fix(tool): precise state.db protection — only block destructive ops on the live file, not read-only mentions
  3. fix(state): refuse journal downgrade when on-disk mode is unknown
  4. fix(state): commit pending state.db hardening batch — write lock, NUL-token guard, WAL fallback tests
  5. test(state): close() must never issue TRUNCATE/RESTART checkpoint
  6. fix(state): reconnect self-heal for runtime connection corruption
  7. chore: drop terminal_tool.py.bak-state-db-protect leftover
  8. fix(state): restore WalUnsupportedError for unknown-mode + require_wal — fixes dead code introduced by the upstream merge (merged existing is None guard shadowed the local branch)

Testing

  • tests/test_hermes_state_wal_fallback.py, test_wal_checkpoint_strategy.py, test_hermes_state.py, test_sqlite_wal_reset_gate.py: 269 passed
  • State-db repair/zeroed/readonly/compression suites: 41 passed, 8 skipped

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins tool/terminal Terminal execution and process management sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages needs-decision Awaiting maintainer decision before any implementation labels Aug 9, 2026
zhouou6 added 2 commits August 10, 2026 09:40
…ections, reconnect self-heal

Production hardening for the canonical session store (state.db), fixing
corruption classes observed on a live v0.20.0 deployment:

- Runtime connection corruption ("file is not a database"): a sibling
  process replacing/truncating the backing file breaks the live
  connection. Add a bounded one-shot reconnect self-heal on the write
  path.
- Mixed journal-mode corruption: journal_mode=DELETE applied while
  sibling connections hold WAL open unlinked live -wal/-shm files.
  Refuse to downgrade when on-disk mode is unknown; raise
  WalUnsupportedError when require_wal is set.
- No write-level serialization: concurrent writers could interleave.
  Add state_db_write_lock (process-local write lock).
- NUL-token guard: decoded binary bytes tokenized into bogus script
  paths crashed Path.expanduser() when HOME is unset.
- close() checkpoint safety: close() must never issue TRUNCATE/RESTART
  checkpoint, only PASSIVE.
- PRAGMA journal_mode reads retry transient disk i/o error; short-lived
  ledger connections verify WAL ownership instead of re-running
  journal_mode=WAL against the gateway's live WAL/SHM files.
- terminal_tool blocks deleting/moving the live state.db from inside
  the gateway process.
…t timeout

The local busy_timeout=5000 patch (PRAGMA busy_timeout + 5s connect
timeout) made SQLite's built-in busy handler silently wait out lock
holds, bypassing _execute_write's jittered patience loop entirely:
test_write_lock_patience's 0.2s patience contract (raise
OperationalError naming the lock holder) never fired because SQLite
waited up to 5s and the write succeeded. Restore upstream's timeout=1.0
and drop the PRAGMA so patience semantics stay intact.
teknium1 pushed a commit that referenced this pull request Aug 13, 2026
…try transient EIO on journal-mode probe

Salvaged remainder of PR #82280 (state.db hardening rollup):

- Runtime connection corruption: a sibling process replacing/truncating
  the backing file breaks the live write connection — every subsequent
  write raises 'file is not a database' and the gateway wedges
  permanently (messages pile up in memory). Add a bounded one-shot
  reconnect on the write path: close the broken connection, reopen the
  DB file (re-running WAL activation + schema reconciliation), retry
  the failed write once.
- _on_disk_journal_mode: retry transient 'disk i/o error' (virtualized
  block devices) a few times before returning None, so a one-shot EIO
  doesn't push callers onto the fail-closed unknown-mode branch.

The rollup's write-lock machinery, checkpoint-strategy changes, and
repair serialization are intentionally NOT included — superseded by
PRs #84277 and #69609, or wrong-direction per the POSIX
lock-cancellation findings (#71724 lineage).
@shali10

shali10 commented Aug 13, 2026

Copy link
Copy Markdown
Author

Closing as superseded — upstream has since implemented the core protections itself (reconnect self-heal, WalUnsupportedError, PASSIVE-only checkpoints, write serialization, NUL/zeroed-db guards) across #69603/#45383/#68474 and related commits. Any remaining delta will be proposed as a fresh PR against current main.

@shali10

shali10 commented Aug 13, 2026

Copy link
Copy Markdown
Author

Closing as superseded — upstream has since implemented the core protections itself (reconnect self-heal, WalUnsupportedError, PASSIVE-only checkpoints, write serialization, NUL/zeroed-db guards). Remaining delta will be proposed as a fresh PR against current main.

@shali10 shali10 closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/terminal Terminal execution and process management type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants