nesquena/hermes-webui

Stale cancel recovery appends duplicate recovered turn after persisted final answer

Open

#6,366 opened on Jul 20, 2026

View on GitHub
 (4 comments) (0 reactions) (0 assignees)Python (1,700 forks)github user discovery
bughelp wantedprioritysessionstreaming

Repository metrics

Stars
 (13,820 stars)
PR merge metrics
 (PR metrics pending)

Description

Bug description

A completed assistant turn can be persisted correctly, then a later cancelled/recovery path appends a duplicate recovered user turn, a matching _partial assistant row, and a synthetic No response from provider error after the valid final answer.

This is a persisted transcript corruption, not only a live DOM/rendering problem: the duplicate rows are present in the session sidecar and canonical session data, and survive reload.

This appears to be a remaining cancel / stale-pending recovery variant of #5575 and #5686. The existing settled-answer guards did not prevent recovery from appending after a final assistant row was already committed.

Actual persisted shape

Sanitized tail from one affected session:

index 166  assistant  final answer, content_len=1569, id=223
index 167  user       _recovered=true, content_len=46
index 168  assistant  _partial=true, content_len=1569
index 169  assistant  _error=true, generic "No response from provider"

Important details:

  • row 168 has exactly the same content length as the already committed final assistant row 166;
  • the recovered user row is appended after the valid final response;
  • the synthetic error is persisted after both;
  • after settlement, active_stream_id, pending_user_message, and pending_started_at are cleared;
  • reload does not repair the ordering because the duplicate/error tail is durable.

Run-journal evidence

The session has separate terminal journal records with this sanitized sequence:

stream 9af9fafd: cancel, terminal=true, type=cancelled
stream 12d7dc67: done, terminal=true
stream 12d7dc67: stream_end, terminal=true
stream 3ff07304: apperror, terminal=true, type=no_response

Chronological timestamps show the cancel terminal record first, a later stream committing done + stream_end, and then a still-later synthetic no_response settlement. The durable transcript ends with the valid final answer followed by the recovered/partial/error tail shown above.

Expected behavior

Once a current-turn final assistant response is durably committed, stale cancellation or pending-turn recovery for that same turn must not append:

  • another copy of the user message;
  • a matching _partial assistant response;
  • a synthetic No response from provider card.

Recovery should remain available for a genuinely interrupted turn that has no committed final response.

Likely boundary

Before _append_recovered_pending_turn() and journal partial recovery append on a non-completed terminal journal, recovery should re-check persisted truth for the exact pending/current turn.

If that turn already has a durable, non-partial, non-error final assistant row, the stale pending/recovery state should be cleared without appending recovered rows.

This needs current-turn identity or another fail-closed boundary. Prompt-text-only matching could incorrectly suppress a genuine failure when prompts repeat.

Relationship to existing work

  • #5575 documents a recovery-path variant and a proposed narrow local fix for a recovered final answer over a matching stale _partial + generated No response from provider tail.
  • #5686 covers completed assistant output followed by a false No response from provider settlement.
  • #5902 and #6124 address checkpointed-current-user and result-provenance variants.
  • #3400 and #2361 track the broader Live-to-Final and run-state consistency contracts.

This report is specifically about durable stale recovery append after a final answer is already persisted, including a duplicate _recovered user row.

Suggested regression test

Exercise this order for one logical turn:

  1. persist the current user checkpoint/pending state;
  2. produce a cancellation/SSE-interruption terminal journal;
  3. durably commit one final assistant answer;
  4. run stale pending/journal recovery;
  5. assert the persisted transcript contains exactly one user turn and one final assistant answer;
  6. assert no _recovered duplicate, matching _partial, or synthetic no_response row is appended;
  7. retain separate negative coverage proving a genuinely interrupted turn with no committed final answer is still recovered.

Environment

  • Hermes WebUI: v0.52.76, commit 3c9304aef5d8fa2a184d0506eab0b4bf898cc079
  • Hermes Agent: v0.18.2 (2026.7.7.2)
  • OS: Linux 6.8.0-106-generic, x86_64

No local WebUI patch or recovery-related config workaround was active when the incident was captured.

Contributor guide