fix(agent): stop reference-only compaction handoff from becoming the active turn (salvage #80696, fixes #80622) - #81070
Merged
Conversation
…active turn After a completed assistant stop, a standalone CONTEXT COMPACTION handoff could occupy the sole user slot and resume stale Historical Task Snapshot work with no new human ask. Guard post-compaction continues, hide standalone handoffs from session dispatch, and harden SUMMARY_PREFIX for the empty-after-handoff case (NousResearch#80622).
Pin NousResearch#80622 invariants: handoff alone must not drive a model call after stop, pending real users are restored, and synthetic compaction rows are never treated as user-originated turns. Also give micro-compaction enough passes to pay back the longer SUMMARY_PREFIX marker overhead.
…tes, refund ordering, prompt carve-out, honest skip response Follow-ups on top of the salvaged NousResearch#80696 fix (review findings): - Sibling sites: rollback.restore, gateway /retry, CLI /retry and /undo N, and both CLI resume turn counters now use is_user_originated_turn so legacy-persisted standalone handoffs (durable role=user, no display_kind) can never be truncation targets or counted as user turns (NousResearch#80622 suggested regression 4, dispatcher-wide). - Site-1 guard: hoist the api_call_count decrement + iteration-budget refund above the break so a skipped turn no longer leaks a budget unit and finalize_turn logs the true call count (matches the ollama early-exit and the site-2 sibling). - Site-2 guard: run the handoff guard BEFORE reanchoring so a restored user ask is what the anchor lands on, not a stale pre-restore index. - SUMMARY_PREFIX: add the mid-tool-loop carve-out the code-side guard already implements, so a literal-minded model doesn't halt an in-flight exchange after in-place compaction. - Skip path returns a short compaction status instead of replaying the previous turn's answer (finalize_turn would append it as a fresh assistant row — duplicate prose in transcript and delivery).
…an, canonical strip delegation Review-pass follow-ups (three parallel reviewers, findings verified): - hermes_state_search.py list_recent_user_messages now drops legacy standalone compaction handoffs in the decode loop (SQL can't see them: durable role=user, no display_kind). Closes the /undo N pairing skew where the in-memory count (new predicate) and the DB soft-delete pick (old predicate) targeted different turns on legacy sessions. Fetches with headroom so the requested limit is still honored. 3 new tests, mutation-checked (no-op'ing the skip fails 2/3). - _should_skip_model_call_for_reference_handoff: single drive-check scan (was two — once inside the restore helper, once after); the restore helper no longer re-scans and its return value now decides the verdict. - _final_response_from_messages replaced by the _HANDOFF_SKIP_FINAL_RESPONSE constant it always returned (parameter was unused). - _handoff_carries_live_user_content delegates to the canonical _strip_context_summary_handoff_message — also fixes the edge where a merged-shaped row with an EMPTY preserved prior tail was wrongly treated as carrying live content. - Site-level guard test for rollback.restore with a legacy handoff row (predicate-in-context, complements the unit tests).
4 tasks
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A reference-only
[CONTEXT COMPACTION — REFERENCE ONLY]handoff can no longer become the active user turn. Fixes #80622: after an assistant response completed withfinish_reason=stop, compaction inserted a standalonerole=userhandoff carrying a Historical Task Snapshot, no new human ask followed, and the agent started a fresh tool-calling turn that resumed already-completed work.Salvage of #80696 by @HexLab98 — both commits cherry-picked with authorship preserved, plus one follow-up commit finishing the bug class (review findings).
Who hits this
Long tool-calling sessions on any surface (reported from the Linux Desktop app): the user watches the agent silently restart work it already finished, with no new request — and
/retry//undocould target the synthetic handoff instead of the real last exchange.Changes (contributor commits)
agent/context_compressor.py—reference_handoff_would_drive_next_model_call()guard predicate (mid-tool-loop exempt);is_user_originated_turn()for dispatchers; SUMMARY_PREFIX gains an explicit "if no user message appears after this summary, do nothing" clause with the prior generation byte-frozen in_HISTORICAL_SUMMARY_PREFIXES(verified byte-identical to upstream/main's live prefix — old-summary renormalization safe).agent/conversation_loop.py— 3 post-compaction continue sites skip the model call when only a handoff would drive it; a pending real user ask is restored first.agent/turn_context.py— reanchor fallback refuses non-user-originated rows.run_agent.py— standalone handoffs always projectdisplay_kind=hidden.tui_gatewayundo/retry walkers use the new predicate; 184-line regression test file.Follow-up commits (finishing the bug class + review folds)
Commit 2 (bug class):
is_user_originated_turn:rollback.restoretruncation (whose comment claimed predicate parity that was no longer true), gateway/retry, CLI/retry+/undo N, and both CLI resume turn counters — legacy sessions persisted pre-fix carry handoffs with nodisplay_kind, so predicate-level exclusion is required (issue's suggested regression Fix terminal interactivity #4, dispatcher-wide).Commit 3 (simplify-pass folds, three parallel reviewers):
list_recent_user_messages(hermes_state_search.py) now drops legacy standalone handoffs in its decode loop — SQL can't see them (role=user, no display_kind). Closes the/undo Npairing skew where the in-memory count (new predicate) and the DB soft-delete pick (old predicate) targeted different turns on legacy sessions. Fetches with headroom so the requested limit is honored; 3 new tests, mutation-checked._final_response_from_messages→_HANDOFF_SKIP_FINAL_RESPONSEconstant (the parameter was unused)._handoff_carries_live_user_contentdelegates to the canonical_strip_context_summary_handoff_message— also fixes the edge where a merged-shaped row with an EMPTY preserved prior tail was wrongly treated as carrying live content.Validation
Consecutive-user-row concern from review: the restore append follows the existing
_insert_real_user_anchorprecedent —repair_message_sequencemerges adjacent user rows before every API call, keeping role alternation valid.Fixes #80622. Based on #80696 by @HexLab98 — commits cherry-picked to preserve authorship.