5 Commits

Author SHA1 Message Date
teknium1
ad01b93b26 fix(state): /undo and /retry work again after a turn that got no reply
A turn that ends with no assistant reply (non-retryable provider error,
interrupt) leaves an unanswered user row in the transcript. The next ask
makes the live history `user;user`, and the pre-request alternation repair
merges the pair into ONE turn in place while both rows stay stored. From
then on the warm history has one user turn fewer than the durable
projection, and `rewind_user_turn` refused every /undo N and /retry with
"session history changed before the rewind could be persisted" for the
rest of the session; resume re-merges, so it never healed.

Rewind now addresses turns on the same alternation-repaired projection the
live process holds (the load `get_resume_conversations` already uses). The
merged turn keeps the first row's identity, so /undo 1 soft-deletes both
stored rows of the pair and the reply; the in-txn payload pin compares
against the STORED first row, so the pin stays strict (no prefix matching).
The gateway surface, which has no warm history, gets the same turn
boundaries as CLI and TUI instead of undoing only the second half.

Slimmer redo of #115511 by @MohamadKanso: same direction (repaired durable
load), without the `_unmerged_content` marker on live history dicts and
without relaxing `_split_rewind_target` to a startswith match.

Co-authored-by: Mohamad Kanso <91088196+MohamadKanso@users.noreply.github.com>
2026-09-20 12:12:26 -07:00
kshitijk4poor
b3534f3575 perf(state): counters from the rows in hand, bounded identity read, one row-identity helper
Efficiency gate: replace_messages had swapped the free insert-return counters for a full
active-set rescan (JSON parse per row) on every branch, slowing the untouched compress/ACP
path; the counts are now kept-prefix (from the fetched rows) + inserted. The identity SELECT
is bounded to len(messages)+1 rows — the prefix plus the row that anchors the archive UPDATE.
_row_identity is the one place the compared columns are spelled; _kept_live_prefix became
_stamp_kept_live_prefix (it mutates _row_id); rewind's _comparison_content delegates to the
shared _loaded_view_content.
2026-09-20 16:47:29 +05:30
teknium1
82d03714ed fix(state): durable _row_id adoption after rewind is opt-in (TUI) instead of every warm caller
On origin/main only the TUI copied durable `_row_id`s onto the installed warm
prefix (its clients address follow-ups by row); folding the loop into
`rewind_user_turn` made CLI /undo grow `_row_id` on a resumed history that never
had it. Live CLI rows already carry ids from the flush, so for them it was a
no-op, but a resumed transcript changed shape. The loop now runs only with
`adopt_row_ids=True`, which the TUI passes; the CLI history shape is unchanged.

Review follow-up on #109610.
2026-09-13 05:20:26 -07:00
teknium1
e3281fe249 fix(state): gateway /retry re-sends a multi-part carrier's stored bytes, not a "\n"-joined view
`rewind_user_turn` called `retryable_user_text` for validation only and returned
`flatten_message_text(...)` (`"\n".join` of the parts) as `live_text`, which the
gateway used as the re-sent prompt: `[{text:"a"},{text:"b"}]` went back to the
model as `"a\nb"` where origin/main sent `"ab"`. When `require_retryable` the
outcome now carries the lossless `"".join` (wire bytes == stored bytes); the
display flattening stays for /undo prefill.

Review follow-up on #109610.
2026-09-13 05:20:26 -07:00
teknium1
c3de99bbf0 refactor(state): one carrier-aware user-turn rewind behind CLI /undo, /retry, gateway and TUI
CLI `_rewind_persisted_user_turn`, TUI `_rewind_active_session_history` and gateway
`rewind_session` each re-ran get_active_message_ids -> get_messages_as_conversation ->
split_user_originated_turn -> rewind_to_message with their own warm/durable comparison
helpers and three different out-of-range contracts (RuntimeError / ValueError / None).

The durable transcript is the authority for a rewind, so the implementation now lives
with the data: `SessionDB.rewind_user_turn` (hermes_state_rewind.py) with one typed
out-of-range error (`RewindTargetUnavailableError`). Surfaces keep only lock, eviction
and rendering glue and map that error to their own message.
2026-09-13 05:20:26 -07:00