fix(gateway): restore the state.db periodic maintenance reverted by #64 - #81
Conversation
PR #64 was replayed onto main with the wrong merge base (its parent branch's TIP rather than the true fork point). Files main had gained after that branch forked therefore looked like deletions, so merging #64 silently reverted #79: * gateway/run.py — _run_state_db_maintenance_once() refactor undone * hermes_cli/config_defaults.py — housekeeping comment reverted * tests/gateway/test_state_db_periodic_maintenance.py — deleted This re-applies #79 verbatim onto current main. The LSP work from #63 and #64 (including sessions.max_clients) is untouched. Audited: #63 and #65 match their original diffstats exactly; #64 was the only bad replay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c905ca0dcc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| finally: | ||
| _adb.close() |
There was a problem hiding this comment.
Avoid checkpointing the live WAL on every maintenance tick
When either automatic archive or prune is enabled, this hourly path always calls _adb.close(), even when the maybe_* methods immediately skip because min_interval_hours has not elapsed. SessionDB.close() unconditionally executes PRAGMA wal_checkpoint(TRUNCATE) for writable connections (hermes_state.py:2716-2752), so a gateway with a large or busy WAL can contend with active transcript writers every hour despite this change's stated goal of avoiding disruptive mid-traffic database maintenance. The periodic connection needs a close path that does not request a TRUNCATE checkpoint; the mock-only test currently cannot expose this real SQLite side effect.
AGENTS.md reference: AGENTS.md:L84-L87
Useful? React with 👍 / 👎.
| _adb.maybe_auto_prune_and_vacuum( | ||
| retention_days=int(_sess_cfg.get("retention_days", 90)), | ||
| min_interval_hours=int(_sess_cfg.get("min_interval_hours", 24)), | ||
| min_vacuum_interval_days=int( | ||
| _sess_cfg.get("min_vacuum_interval_days", 30) | ||
| ), | ||
| vacuum=False, |
There was a problem hiding this comment.
Preserve a pending VACUUM after live pruning
When this live pass actually deletes sessions, maybe_auto_prune_and_vacuum() records last_auto_prune but cannot VACUUM because vacuum=False. The only later VACUUM path is startup, yet that helper either skips while this timestamp is fresh or sees pruned == 0 after the live pass already removed the rows, and it only vacuums when the same invocation reports pruned > 0 (hermes_state.py:9080-9121). Consequently a long-lived gateway—or one restarted within the configured interval—can keep vacuum_after_prune enabled while never returning the deleted pages to disk, leaving the multi-gigabyte file this change is intended to address. Record pending reclamation separately and consume it during the next safe startup pass.
AGENTS.md reference: AGENTS.md:L54-L57
Useful? React with 👍 / 👎.
| # per (roughly) min_interval_hours at CLI/gateway/cron startup, and | ||
| # on the resident gateway's hourly housekeeping sweep (per served | ||
| # profile, without VACUUM) — so a gateway that stays up for weeks | ||
| # keeps pruning on cadence instead of only at its next restart. |
There was a problem hiding this comment.
Update the cleanup guide for the live pruning cadence
This changes the retention contract from startup-only pruning to pruning by an already-running gateway, but the user guide still says sessions are pruned only at CLI/gateway startup (website/docs/user-guide/sessions.md:716, with the same statement in the Chinese guide at line 581). An operator can therefore enable sessions.auto_prune while planning for deletion at the next controlled restart, only to have the resident gateway irreversibly delete eligible history on its next housekeeping pass. Update the user-facing cleanup documentation alongside this new cadence.
Useful? React with 👍 / 👎.
What happened
While landing the stacked LSP backlog, PR #64 was replayed onto
mainwith the wrong merge base — its parent branch's tip instead of the
true fork point. Files that
maingained after that branch forkedtherefore presented as deletions in the 3-way merge, so merging #64
silently reverted #79:
gateway/run.py— the_run_state_db_maintenance_once()refactor undone (158 lines)hermes_cli/config_defaults.py— housekeeping comment revertedtests/gateway/test_state_db_periodic_maintenance.py— deleted (266 lines)This matters: #79 is what keeps
state.dbpruning on the housekeepingcadence rather than only at process startup — the fix for the 2026-08-15
disk-pressure incident where
state.dbreached 3 GB.What this does
Re-applies #79 verbatim onto current
main. The restoredgateway/run.pyand test file are byte-identical to their landed state;
max_clientsandthe rest of the #63/#64 LSP work are preserved.
Audit
Every merge in this batch was compared against its original PR diffstat:
#64 was the only bad replay.
git diff --diff-filter=D dd73d9c37 origin/mainconfirms exactly one file was deleted repo-wide.
🤖 Generated with Claude Code