Skip to content

fix(state): heal durable alternation violations at the restore boundary - #64935

Closed
Rival wants to merge 1 commit into
NousResearch:mainfrom
Rival:fix/restore-alternation-repair
Closed

fix(state): heal durable alternation violations at the restore boundary#64935
Rival wants to merge 1 commit into
NousResearch:mainfrom
Rival:fix/restore-alternation-repair

Conversation

@Rival

@Rival Rival commented Jul 15, 2026

Copy link
Copy Markdown

A turn that persists a user row with no assistant row (suppressed reply, or
two concurrent turns interleaving their flushes — see #64934) leaves a
user;user pair in state.db. The defensive pre-request
repair_message_sequence then re-fires on every request for the rest of the
session's life — it mutates only the per-request list, never the stored
transcript. Observed: 25×/day on one long-lived session, 297 occurrences
over two weeks.

This adds repair_alternation (default False) to
get_messages_as_conversation and passes it from the three live-replay
restore sites (gateway load_transcript, CLI session resume ×2).
Inspection/export consumers (trace upload, context guard, api_server
history) keep the verbatim default.

4 new tests (tests/hermes_state/test_restore_alternation_repair.py):
verbatim by default; user pair merged in order with no input lost; repaired
load yields zero further pre-request repairs; no-op on clean transcripts.
Neighboring state suites pass (411).

🤖 Generated with Claude Code

A turn that persists a user row with no assistant row (suppressed reply,
or two concurrent turns interleaving their flushes) leaves a user;user
pair in state.db. The defensive pre-request repair_message_sequence then
re-fires on EVERY request for the rest of the session's life — it mutates
only the per-request list, never the stored transcript.

Add repair_alternation (default False) to get_messages_as_conversation
and pass it from the three live-replay restore sites (gateway
load_transcript, CLI session resume x2). Inspection/export consumers
(trace upload, context guard, api_server history) keep the verbatim
default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state P2 Medium — degraded but workaround exists labels Jul 15, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for isolating the restore-boundary mitigation and preserving verbatim inspection/export reads. The underlying condition remains on current main: agent/conversation_loop.py:787-804 repairs malformed alternation only immediately before an API call.

Problems

  • The change does not cover all live replay paths. Interactive CLI /resume still restores raw history at hermes_cli/cli_commands_mixin.py:774-789. TUI/Desktop normal resume uses raw history at tui_gateway/server.py:5764-5785; ACP restores it at acp_adapter/session.py:539-563; and API session chat passes the raw load to _run_agent at gateway/platforms/api_server.py:1850-1858 and :2062-2068. Those paths can retain the repeated pre-request repair described in #64934.

Suggested changes

  • Audit and cover every model-fed restore path, while leaving display/export consumers verbatim.
  • Add integration coverage for interactive CLI /resume and a non-CLI replay path, in addition to the SessionDB-level tests.

Automated hermes-sweeper review.

Comment thread hermes_state.py
# resumes clean even if stray rows exist.
messages = _strip_background_review_harness(messages)
if repair_alternation and messages:
# Lazy import: hermes_state already depends on agent.* (see

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please ensure this repair is also selected at every model-fed restore boundary. Current main has additional live paths that still load raw history, including CLI /resume (hermes_cli/cli_commands_mixin.py:774), TUI/Desktop resume (tui_gateway/server.py:5764), ACP restore (acp_adapter/session.py:539), and API session chat (gateway/platforms/api_server.py:1855).

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 16, 2026
teknium1 added a commit that referenced this pull request Jul 16, 2026
teknium1 added a commit that referenced this pull request Jul 16, 2026
teknium1 added a commit that referenced this pull request Jul 16, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #65492 (restore-boundary repair) — your commit was cherry-picked onto current main with authorship preserved. The companion tripwire (#64936) landed via PR #65499 the same way.

Exceptional report on #64934, by the way — the timeline, row-level evidence, and the honest scoping ("cause-agnostic fixes now, serialization once the tripwire names the route") made this an easy salvage. One follow-up we added during CI: an old test fixture seeded a 120-row all-user transcript and asserted the restore length, which your restore-time repair correctly collapsed — the fixture now alternates roles.

The serialization patch offer stands noted on #64934; once the tripwire fires in a deployment and names the dispatch route, that's the natural next step.

@teknium1 teknium1 closed this Jul 16, 2026
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades 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