Skip to content

fix(agent): stop reference-only compaction handoff from becoming the active turn (salvage #80696, fixes #80622) - #81070

Merged
kshitijk4poor merged 4 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/80696
Aug 7, 2026
Merged

fix(agent): stop reference-only compaction handoff from becoming the active turn (salvage #80696, fixes #80622)#81070
kshitijk4poor merged 4 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/80696

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

A reference-only [CONTEXT COMPACTION — REFERENCE ONLY] handoff can no longer become the active user turn. Fixes #80622: after an assistant response completed with finish_reason=stop, compaction inserted a standalone role=user handoff 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//undo could target the synthetic handoff instead of the real last exchange.

Changes (contributor commits)

  • agent/context_compressor.pyreference_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 project display_kind=hidden.
  • tui_gateway undo/retry walkers use the new predicate; 184-line regression test file.

Follow-up commits (finishing the bug class + review folds)

Commit 2 (bug class):

  • 5 more sibling sites converted to is_user_originated_turn: rollback.restore truncation (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 no display_kind, so predicate-level exclusion is required (issue's suggested regression Fix terminal interactivity #4, dispatcher-wide).
  • Refund ordering at guard site 1: the api_call_count decrement + iteration-budget refund now happen before the break (previously each skipped turn leaked one budget unit for the agent's lifetime).
  • Guard-before-reanchor at site 2 so a restored user ask is what the anchor lands on.
  • Prompt carve-out: SUMMARY_PREFIX's new clause now states the mid-tool-loop exception the code-side guard already implements, so a literal-minded model doesn't halt an in-flight exchange.
  • Honest skip response: the skipped turn returns a short compaction status instead of replaying the previous turn's answer (finalize_turn's Gateway can deliver assistant response without persisting assistant row #43849 chokepoint would have appended the old prose as a fresh assistant row — duplicated in the transcript and re-delivered to the user).

Commit 3 (simplify-pass folds, three parallel reviewers):

  • DB picker parity: 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 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 honored; 3 new tests, mutation-checked.
  • Single scan: the skip guard now runs the drive-check once (was twice — inside the restore helper and again after) and the restore helper's return value decides the verdict.
  • _final_response_from_messages_HANDOFF_SKIP_FINAL_RESPONSE constant (the 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: rollback.restore with a legacy handoff row.

Validation

Check Result
PR + compression + tui_gateway suites (post-fold) 448 passed
Full tests/agent/ vs upstream/main baseline identical failure sets (104 pre-existing env failures on both, 0 new)
E2E probe (sole-handoff skip, honest status, restore, mid-tool-loop, predicate, carve-out live-only, strip delegation) all pass
SUMMARY_PREFIX frozen-generation byte-identity vs upstream/main verified programmatically
Mutation checks (revert agent/ to pre-fix; no-op the DB-picker skip) guard tests fail on base, green on stack
ruff (all touched files) clean

Consecutive-user-row concern from review: the restore append follows the existing _insert_real_user_anchor precedent — repair_message_sequence merges adjacent user rows before every API call, keeping role alternation valid.

Fixes #80622. Based on #80696 by @HexLab98 — commits cherry-picked to preserve authorship.

HexLab98 and others added 4 commits August 7, 2026 18:41
…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).
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) area/compression Context compression and continuation sessions sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 7, 2026
@kshitijk4poor
kshitijk4poor merged commit fecba5a into NousResearch:main Aug 7, 2026
50 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/80696 branch August 7, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compression Context compression and continuation sessions comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) P1 High — major feature broken, no workaround sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression: reference-only compaction handoff can become the active turn and resume completed work

3 participants