nesquena/hermes-webui

Stale cancel recovery appends duplicate recovered turn after persisted final answer

Open

#6,366 opened on Jul 20, 2026

 (5 comments) (0 reactions) (0 assignees)Python (2,449 forks)github user discovery
bughelp wantedneedinfoprioritysessionsprint-candidatestreaming

Repository metrics

Stars
 (17,688 stars)
PR merge metrics
 (Avg merge 14h 31m) (314 merged PRs in 30d)

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