c7429f60ca
test_no_locked_readers_gate.py (#97676) parses hermes_state.py's SessionDB class body with ast and flags any method that holds the writer lock around a pure-read query — Pattern C, where every concurrent turn's persistence convoys behind an unrelated read. #97676 converted 39 such methods and closed detection blind spots for alias/variable-SQL readers. But SessionDB is declared as `class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)`, and the gate only ever opened hermes_state.py — it never parsed the three mixin files those base classes are defined in, so a locked reader declared there was structurally invisible to the scanner regardless of how good the alias/variable-SQL detection got. Applying the gate's exact scanning logic to the three mixin files directly turns up 9 genuine pure-read methods still holding the writer lock, none in #97676's converted list: - hermes_state_search.py: _fts_teardown_trash_step, fts_optimize_available, optimize_fts_storage, list_recent_user_messages - hermes_state_portability.py: distinct_session_cwds, list_cron_job_runs, _get_session_rich_rows_batch, list_skill_scaffolded_sessions, get_first_assistant_text _get_session_rich_rows_batch is a hot path: it backs list_sessions_rich's compression-tip resolution and the web server's session-search hydration across every gateway install — its own docstring already claims "same read-your-writes guarantee as list_sessions_rich", but list_sessions_rich was already using _read_ctx() (its guarantee comes from flush_token_counts() before the read, not from holding the writer lock) while this method's implementation never caught up to match. Converted all 9 to `with self._read_ctx() as conn:`, the exact pattern #97676 used, verified each is a genuine pure read with no hidden writes by tracing every helper call it makes. Extended the gate itself (_ALL_STATE_SOURCES) to scan all three mixin files under their own class names, plus hermes_state.py, so this blind spot can't silently reopen. Added a regression test (test_scan_all_state_sources_visits_every_mixin_file) that plants a synthetic violation in a mixin-shaped file and asserts the scanner still catches it — a change that reverts the file list back to one file passes the existing sabotage test but fails this one. Mutation-verified: with the gate's new scope but the old (unconverted) mixin sources, test_no_locked_pure_readers fails and names all 9 real violations with correct file/line. Restored the fix; it passes clean.