Skip to content

fix(dashboard): return 503, not 500, for a deleted WAL or replaced state.db - #121428

Merged
kshitijk4poor merged 5 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/110054-dashboard-deleted-wal-503
Sep 24, 2026
Merged

kshitijk4poor merged 5 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/110054-dashboard-deleted-wal-503

Conversation

@kshitijk4poor

Copy link
Copy Markdown

When state.db's WAL has been deleted or the database file was replaced underneath a running process, the dashboard now returns a 503 with recovery guidance instead of a bare 500. This covers the analytics routes, GET /api/sessions and session lookup. It is the slim, still-missing part of #110054, salvaged from #110073 by @JoaoMarcos44.

Why

#110054: DeletedWalGenerationError and StateDbReplacedError subclass RuntimeError, not sqlite3.DatabaseError. corrupt_store_as_status (analytics) and the sessions router only mapped malformed-sqlite errors to 503, so these two escaped as 500s. On main, a probe against /api/analytics/usage returns 500 for both.

Changes

  • edafec95d0 (@JoaoMarcos44): corrupt_store_as_status also catches StateDbReplacedError (and its DeletedWalGenerationError subclass) and returns 503.
  • c2ed1cff96: the payload no longer tells users to "run hermes doctor --fix". Main's guard text says not to run --fix while holders are live, so every mention of --fix is now negated.
  • 5c26b1c908: an invariant test covering both error types, which also asserts that no --fix instruction is left un-negated.
  • 768555b7a7: the payload is keyed by the existing classify_persistence_error() bucket rather than a new isinstance ladder. Codes are now uniformly state_db_corrupt / state_db_deleted_wal / state_db_replaced; no frontend code keys on them.
  • 2c63a9bd23: GET /api/sessions and _resolve_session_id route the same errors through corrupt_store_as_status. That leaves the malformed-sqlite latch untouched.

Validation

  • Dashboard / web-router tests: 74 passed after rebase on current main.
  • Live probe (temp HOME, real analytics.router): on main both errors return 500; on this branch both return 503 with the right code and only negated --fix text. The controls are identical to main: healthy → 200, malformed → 503 state_db_corrupt, locked → propagates, unrelated RuntimeError → 500.
  • Mutation: reverting the mapping turns the invariant test red, and reverting the sessions arms turns the extended test red.
  • Gate: tests + live probe, deep review, 3 /simplify-code reviewers, fold, final deep review with mutation + 3 reviewers. All PASS / no material findings.
  • Windows footgun scan: 1706 files, clean.

Not in this PR

Credit

Refs #110054

JoaoMarcos44 and others added 5 commits September 24, 2026 16:37
… 503 in corrupt_store_as_status (NousResearch#110054)

Analytics routes open SessionDB under corrupt_store_as_status, which only
caught sqlite3.DatabaseError. DeletedWalGenerationError and
StateDbReplacedError subclass RuntimeError, so a retired WAL generation
escaped the guard and every dashboard poll turned into a 500 storm.
Catch StateDbReplacedError alongside the malformed-image case and return
the same structured 503 payload with detail.error='deleted_wal' /
'state_db_replaced'.

Hunk applied from PR NousResearch#110054 commit 41e2b12 (web-router mapping only;
the holder-termination path from that PR is left to the maintainer).
…placed 503 payload

The contributor's 503 messages told the user to "click Recover or run
`hermes doctor --fix`". Main's deleted_wal/replaced explainers
(agent/turn_explainers.py, hermes_state_errors.py) say the opposite:
running `doctor --fix` while a holder process is alive repairs the wrong
generation in place, and the Desktop Recover button from NousResearch#110054 was not
taken. Hoist the two payloads into module constants next to
CORRUPT_STORE_DETAIL and reuse the explainer guidance (quit every Hermes
process, run `hermes doctor`, do NOT run `--fix`); the log line likewise
points at plain `hermes doctor`.

Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
One test in the existing analytics corrupt-store file: both RuntimeError
subclasses become a structured 503 with the right detail.error and path,
the message never nudges `doctor --fix` except as a negation, and a
busy/locked sqlite3.OperationalError still propagates. Replaces the PR's
test_corrupt_store_as_status_handles_deleted_wal_and_replaced_errors,
which asserted the removed `--fix` wording.

Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
…r bucket

The deleted_wal/replaced payload selection re-derived the type-ordered bucket
table that hermes_state_errors._PERSISTENCE_CAUSE_BY_TYPE already owns; a
future bucket could silently diverge from the dashboard mapping. Look the
payload up by cause bucket instead (default: the corrupt payload, which also
covers an FTS-scoped malformed image). Proven behaviour-equivalent for every
exception that passes the guard (both replaced-family types and subclasses,
malformed sqlite errors incl. StateDbCorruptError and fts_index-scoped ones);
busy/locked and unrelated errors still propagate.

Also: one `state_db_*` naming scheme for the `error` codes
(`deleted_wal` -> `state_db_deleted_wal`; no consumer keys on it —
web/src/lib/api.ts only branches on the auth codes), drop the redundant
sqlite3.DatabaseError isinstance that is_malformed_db_error already performs,
and make the test assert the invariant (every `--fix` mention is negated)
rather than the exact wording.
…api/sessions too

GET /api/sessions and _resolve_session_id caught only sqlite3.DatabaseError,
so StateDbReplacedError / DeletedWalGenerationError (RuntimeError family)
fell through to the bare `except Exception` -> 500 "Internal server error" —
the same mis-mapping NousResearch#110054 fixed for the analytics reads. Route that
exception family through the existing corrupt_store_as_status so the sessions
list and detail routes return the same structured 503 payload; the sqlite
arms (busy -> 503, malformed -> latch + 503) are untouched, which is why the
mapping is added as a sibling arm instead of wrapping the read (that would
skip note_storage_error for a malformed image).

The kept invariant test now also exercises both sessions.py paths; it fails
without this change (raw DeletedWalGenerationError escapes the resolver,
the route returns 500).
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) September 24, 2026 11:09
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 24, 2026
@kshitijk4poor
kshitijk4poor merged commit 4cc4072 into NousResearch:main Sep 24, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants