Stale cancel recovery appends duplicate recovered turn after persisted final answer
#6,366 opened on Jul 20, 2026
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, andpending_started_atare 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
_partialassistant response; - a synthetic
No response from providercard.
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+ generatedNo response from providertail. - #5686 covers completed assistant output followed by a false
No response from providersettlement. - #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:
- persist the current user checkpoint/pending state;
- produce a cancellation/SSE-interruption terminal journal;
- durably commit one final assistant answer;
- run stale pending/journal recovery;
- assert the persisted transcript contains exactly one user turn and one final assistant answer;
- assert no
_recoveredduplicate, matching_partial, or syntheticno_responserow is appended; - retain separate negative coverage proving a genuinely interrupted turn with no committed final answer is still recovered.
Environment
- Hermes WebUI:
v0.52.76, commit3c9304aef5d8fa2a184d0506eab0b4bf898cc079 - 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.