Skip to content

fix(cli): hermes sessions list|stats|pinned and a plain hermes doctor no longer open state.db as a writer (#110173, salvage #110186) - #111627

Merged
kshitijk4poor merged 6 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/110186-observational-stores-read-only
Sep 15, 2026
Merged

kshitijk4poor merged 6 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/110186-observational-stores-read-only

Conversation

@kshitijk4poor

Copy link
Copy Markdown

Observational CLI commands inspect a live install without joining the writable WAL lifecycle: hermes sessions list/stats/pinned open state.db read-only, and hermes doctor (without --fix) probes write health through a read-only snapshot when a gateway holds the store.

Fixes #110173 (remaining half; hermes status/insights landed via #110718). Based on #110186 by @KoNit-K (cherry-picked to preserve authorship); #109725 by @Ahmett101 proposed the sessions half earlier and is credited as co-author.

  • hermes_cli/sessions_cmd.py: _OBSERVATIONAL_DB_ACTIONS open SessionDB(read_only=True); a fresh profile with no state.db prints the empty result instead of creating the store; an older schema gets a migration hint instead of a traceback.
  • hermes_cli/doctor_state.py: _write_health_reason probes in place when the holder scan proves the store quiet (as main did), through a read-only backup() snapshot when a live writer holds it, and skips (info line) a held store over 1 GB unless --fix. Connect/backup failures reach the existing classification instead of being reported as FTS corruption.
  • hermes_state_common.read_only_db_uri() replaces four inline mode=ro URIs — two of them (state_db_has_structural_damage, collect_state_db_stats) still used the raw f-string that truncates on ?/# in the home path (the class fixed in 9b419a2).
  • Tests trimmed to 3 invariants (read-only open, missing store stays missing, snapshot-vs-in-place probe routing).

Behaviour changes to note: sessions list/stats/pinned no longer create or migrate state.db; a plain hermes doctor copies a HELD store (≤ 1 GB) to a temp dir before the write probe.

Validation: scripts/run_tests.sh on 18 files — green except two test_hermes_state.py::TestPerformancePragmasEndToEnd cases and one test_doctor_structural_corruption.py case that fail identically on pristine origin/main under this runner (fd-limit / SQLite-version environment). Real-import probe: read-only open in a q?h#z home works and INSERT raises; old URI form → no such table (the truncation). Mutation (main's sessions_cmd.py + doctor_state.py): 5 failed → restored: 5 passed.

KoNit-K and others added 5 commits September 15, 2026 12:04
## What does this PR do?

Makes observational CLI commands open `state.db` in read-only mode, so they can inspect a live Hermes installation without participating in writable WAL lifecycle handling.

### Symptom

Running `hermes status`, `hermes doctor` without `--fix`, `hermes sessions list`, `hermes sessions stats`, or `hermes insights` while a gateway owns the store could open another writable session handle. The live turn could then lose its WAL generation and stop.

### Impact

Users inspecting status or session history during an active turn could lose that in-flight turn and leave the gateway halted until recovery.

### Bug Cause

**Trigger:** observational CLI helpers constructed `SessionDB()` with its writable default.

**Causal chain:**

1. A live gateway holds the `state.db` WAL generation.
2. A nested observational CLI command opens a second writable handle.
3. Writable-handle close behavior can participate in WAL lifecycle work and retire the generation used by the live writer.

**Why it is wrong:** these commands only query state and should not have writer privileges.

**Working sibling / contrast:** repair and mutating session commands still use writable access intentionally.

**Ruled out:** no state schema, migration, or WAL checkpoint implementation changes are included.

### Fix

Routes status, non-fixing doctor state inspection, sessions list/stats, and both insights entrypoints through `SessionDB(read_only=True)`. Repair and mutating paths remain writable, and regression tests cover WAL preservation with a live writer.

## Related Issue

Fixes NousResearch#110173

## Type of Change

- ✅ Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `hermes_cli/status.py`, `hermes_cli/doctor_state.py`, and insights helpers — open observational state readers read-only.
- `hermes_cli/sessions_cmd.py` — make only `list` and `stats` read-only; retain writable access for mutations.
- `tests/hermes_cli/test_observational_sessiondb_modes.py` — verify access modes and a live writer's WAL remains usable.

## How to Test

- ✅ `scripts/run_tests.sh tests/hermes_cli/test_observational_sessiondb_modes.py tests/hermes_cli/test_cli_insights_command.py` — 9 passed.
- ✅ `scripts/run_tests.sh tests/hermes_cli/test_doctor.py tests/hermes_cli/test_doctor_structural_corruption.py tests/hermes_cli/test_sessions_error_exit_codes.py` — 75 passed; two sandbox-only failures came from blocked host process/symlink operations.
- ✅ A live `SessionDB` writer remains able to create and retrieve a session after `sessions stats` reads the store.

## Checklist

### Code

- ✅ I've read the Contributing Guide
- ✅ My commit messages follow Conventional Commits
- ✅ I searched for existing PRs to make sure this isn't a duplicate
- ✅ My PR contains only changes related to this fix
- ✅ I've run relevant tests locally (see How to Test)
- ✅ I've added tests for my changes
- ✅ I've tested on my platform: macOS

### Documentation & Housekeeping

- ✅ Documentation update: N/A
- ✅ `cli-config.yaml.example`: N/A
- ✅ `CONTRIBUTING.md` or `AGENTS.md`: N/A
- ✅ Cross-platform impact considered
- ✅ Tool descriptions/schemas: N/A
- _session_count: back to main's raw sqlite mode=ro COUNT(*) via as_uri() — routing it through SessionDB(read_only=True) both re-introduced the raw f-string URI ('?'/'#' in the home path truncate it) and queries columns (s.archived) an unmigrated store lacks, so doctor would report a healthy DB as broken.
- _write_health_reason: snapshot source URI built with as_uri() for the same reason; the --fix live probe (_db_opens_cleanly runs BEGIN IMMEDIATE) now falls back to the snapshot unless live_writer_holds_db proves the store quiet, matching _state_db_wal — hermes doctor --fix never becomes a second writer against a gateway's state.db (NousResearch#103339).
- SessionDB._connect_read_only: same as_uri() form so every read-only opener is safe in a home containing '?' or '#'.
- test_sessions_export_output_dir: fixture accepts the read_only kwarg the PR introduced.
- Drop the two doctor tests that pinned the SessionDB factory kwargs; main's URI-reserved-chars test covers _session_count.

Co-authored-by: Ahmett101 <Ahmett101@users.noreply.github.com>
- Keep (a) list/stats/pinned open SessionDB(read_only=True) — one parametrized test — and (b) a missing store prints empty results and is never created.
- Drop the insights read-only test (already covered on main), the status test, the mutating-action/live-writer/doctor-isolation tests, and the two doctor factory tests.
- Replace _EmptyObservationalStore + error-string sniffing with a plain '_default_db_path() does not exist' branch printing each action's empty output; the fake-store tests in test_sessions_pin keep working because the branch only runs when the open fails.
…apshot only

read_only_db_uri() replaces four inline mode=ro URI sites (two of which
still used the raw f-string that truncates on ?/# in the home path:
state_db_has_structural_damage and collect_state_db_stats). The doctor
write probe now applies the live-holder gate in both modes: a quiet store
is probed in place as on main, a held store is probed through a read-only
snapshot, and a held store over 1 GB is skipped with an info line unless
--fix is given (the unconditional copy cost one full DB write per plain
doctor run). Connect/backup failures propagate to the existing
classification instead of being reported as FTS write-health failures.
Observational sessions commands print a migration hint instead of a raw
traceback when a read-only opener meets an older schema.

Co-authored-by: Ahmett101 <Ahmett101@users.noreply.github.com>
… migration

Other OperationalErrors (locked, disk I/O) keep their real message.
@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 area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 15, 2026
…tor/repair helpers

hermes_state_common pulls in agent.* at import, so the URI builder moves to
hermes_state_holders (errno/os/sqlite3/pathlib only) where the gateway
readiness probe and backup can adopt it in a follow-up sweep. The doctor
structural-damage branch is one helper instead of two copies, the holder
scan goes through hermes_state_repair._live_writer_holds_db, the migration
hint uses _schema_not_built (the startswith("no such ") check also matched
"no such module: fts5"), and the hermes_state import is hoisted so an import
failure cannot mask itself as UnboundLocalError.
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) September 15, 2026 07:15
@kshitijk4poor
kshitijk4poor merged commit 30a2991 into NousResearch:main Sep 15, 2026
37 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 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.

state.db WAL generation loss: nested read-only CLI subcommands open the session store writable and kill the live turn

2 participants