hermes sessions repair-profiles: detect and repair crossed-profile session state (#88715 PR-6) - #115689
Merged
Conversation
…file durable state The per-profile store model (#88734), the parent-inheritance fence (#88381), profile-stamped topic rows (#76423) and profile-prefixed voice keys (#75198) are all forward-only: they put NEW state under the right profile and refuse to widen existing damage, but nothing walks the stores and settles what earlier releases left crossed. #113884 found 246 sessions stranded that way and could only warn. `hermes sessions repair-profiles` scans every profile's state.db plus the gateway's voice-mode and sessions.json files and names six kinds of crossing: 1. `profile_name` disagreeing with the row's own session key -> relabel; 2. rows physically in another profile's store -> move (all message generations, usage rows, system prompt) to the owning store, parents before children so lineage survives, copy-then-delete so a crash leaves a duplicate the next run settles; 3. `parent_session_id` crossing namespaces -> sever (own identity kept); 4. routing rows outside the default store under multiplexing -> move (an existing row wins); routing rows for a profile that no longer exists -> drop; 5. Telegram topic bindings and voice-mode entries missing their bot's profile -> relabel from the sessions that hold the chat (ambiguous chats reported); 6. sessions.json mirror entries for an unclaimed namespace -> drop (the legacy import re-injects them into routing every boot). Report-only by default. `--apply` refuses while a gateway owns any store, takes a quick snapshot of every store first, and is idempotent. Two cases are reported but never guessed: rows keyed to a profile that does not exist, and `agent:main` rows inside a named profile's store (`--legacy-main rekey|move` says which of the two histories they are). Storage side lives in `hermes_state_profile_repair.py` (SessionDB mixin); orchestration across stores in `hermes_cli/sessions_repair_profiles.py`; the CLI face in `hermes_cli/sessions_cmd_repair_profiles.py` (pre-DB handler: it opens every store itself). Part of #88715 (PR-6). Closes the remediation gap #113884 only warns about.
|
Independent verification on the PR head (d2c4bbc): sessions-repair-profiles suite 4/4 green on Linux. Detecting and repairing crossed-profile durable state gives operators a way back when sessions land under the wrong profile instead of leaving the store tangled. No findings. |
૮ >ﻌ< ა ci reviewran on d2c4bbc — feat(sessions): debug infoCI timingsCI timings · View report · View jobWall time 5m22s vs 6m39s (-19.3%). 7 job(s) slower, 4 faster, 2 unchanged.
|
This was referenced Sep 19, 2026
Closed
12 of 19 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Forward-only fixes leave installs that already crossed profiles carrying wrong rows forever. This adds the repair path the #88715 closure contract asks for.
What it does
hermes sessions repair-profiles [--apply] [--json] [--yes] [--legacy-main report|rekey|move], run across every profile store plusgateway_voice_mode.jsonandsessions.json. Six finders, each with a fixer:session_keynamespace ≠profile_name→ relabelagent:<p>:in rootstate.db, default rows inprofiles/<p>/state.db) → cross-store move (export every message generation + usage + system prompt, import parents-first so lineage survives, then delete; a crash mid-batch leaves a settleable duplicate, never a loss)parent_session_idcrossing namespaces → sever inheritance columnsgateway_routingrows → move or dropsessions.jsonentries whose namespace no served profile claims → dropDry-run by default and a whole-file no-op (proven by table-dump oracle).
--applysnapshots every store first (backup.create_quick_snapshot), refuses under a live gateway before opening anything for write, and is idempotent.Design decisions
agent:main:rows inside a named store are report-only by default: the row cannot say whether it is a standalone gateway's own history (fix(gateway): warn when sessions are stranded under an unclaimed profile namespace #113884, wants rekey) or a default chat that leaked in under a scoped write ([Bug]: Message typed in one Bot Chat persisted & answered by a different bot (cross-profile DB write, v0.21.0) #102157, wants move).--legacy-main rekey|movemakes the operator choose.hermes profile migrate-identity; their routing /sessions.jsonbookkeeping is dropped.Files
hermes_state_profile_repair.py(new mixin onSessionDB),hermes_cli/sessions_repair_profiles.py(new,RepairPlan+_MoveBatch),hermes_cli/sessions_cmd_repair_profiles.py(new CLI face), parser insubcommands/sessions.py, wiring inhermes_state.py/sessions_cmd.py, docs insessions.md+cli-commands.md.Verification
tests/hermes_cli/test_sessions_repair_profiles.py: one fixture with all six defects across three stores; dry-run no-op; apply settles everything and a second run finds nothing; refuses under a live gateway;--legacy-main rekeyadopts fix(gateway): warn when sessions are stranded under an unclaimed profile namespace #113884-style stranded history.scripts/run_tests.shover the new file +tests/hermes_state/+tests/hermes_cli/test_sessions*+test_profile_identity*: 134 files, 1,151 passed, 0 failed.hermes_cli.mainentry point: dry-run → apply → clean second run, snapshots in bothstate-snapshots/.check_compat_pointers, profile-scope lint (advisory P10 on docstrings and the intentional'agent:main:'SQL literal only) clean.Part of #88715 (PR-6 of the identity slate; independent of PR-1..5). Refs #113884, #102157.