fix(compression): merge the todo snapshot into the trailing user turn - #69860
Merged
Conversation
… consecutive user/user turns After context compression, the preserved todo list was unconditionally appended as a standalone user message. When the compressed transcript already ends with a user message (common case), this creates consecutive user/user turns — a role-alternation violation some providers reject. Fix: fold the snapshot into the trailing user message (blank-line separated) when one exists with plain-string content. Falls back to append when the tail is non-user, empty, or has structured (list) content. Rebased on current upstream/main. Closes #53890
…h stale snapshots Follow-up hardening on the salvaged merge-into-trailing-turn fix: - Merge only into REAL user tails (_is_real_user_message probe). Merging into scaffolding tails (continuation marker, summary-as-user handoff) would upgrade them to real-user evidence after SessionDB projection strips the flags, breaking zero-user provenance (#69292 - _is_synthetic_compression_user_turn keys on the TODO_INJECTION_HEADER content marker, which merge-at-tail would bury mid-content). - Strip a previously merged snapshot block before re-injection so repeated boundaries refresh rather than accumulate todo state, and refresh a bare stale snapshot row in place instead of stacking a duplicate (empty/stale-skip semantics from #26981 by @YLChen-007). - Scaffolding tails keep the flagged standalone append (pre-#53890 status quo; adjacent user rows are repaired downstream by repair_message_sequence / _merge_consecutive_roles).
…pshot refresh Covers the follow-up hardening: continuation-marker and summary-as-user tails keep the flagged standalone snapshot (zero-user provenance #69292 verified via _transcript_has_real_user_turn on the projected rows), stale snapshot rows are refreshed in place, a previously merged snapshot is stripped before re-injection, and an all-completed todo store injects nothing (#26981).
Contributor
૮ >ﻌ< ა ci reviewran on 7eb7d0a all good! |
This was referenced Jul 23, 2026
aivrar
added a commit
to aivrar/portable-hermes-agent
that referenced
this pull request
Aug 3, 2026
Backport the todo-snapshot authority fix from NousResearch/hermes-agent#69860 and update brace-expansion to 1.1.17. Co-authored-by: Yingliang Zhang <zhangyingliang@outlook.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
15 tasks
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
After context compression, the preserved todo snapshot is no longer appended as a standalone synthetic
{role:"user"}message that can read as a fresh user instruction — it is folded into the trailing real user turn instead (mirroring the summary merged-into-tail precedent), preserving role alternation and injection authority. Root cause:compress_contextunconditionally appended the todo snapshot as a new user-role row after the compressed transcript, so the todo text became the tail user turn (#26979) and — when the tail was already a user message — created consecutive user/user turns some providers reject.Changes
agent/conversation_compression.py: fold the todo snapshot into a trailing user message when one exists (blank-line separated for strings, appended text block for multimodal lists via_append_text_to_content); fall back to the flagged standalone append otherwise (salvaged from fix(compression): merge todo snapshot into trailing user msg to avoid consecutive user/user turns #53890).agent/conversation_compression.py(follow-up hardening): merge only into REAL user tails — a_is_real_user_messageprobe keeps scaffolding tails (continuation marker, summary-as-user handoff, bare stale snapshot rows) on the flagged standalone path, because merging would bury theTODO_INJECTION_HEADER/ summary-prefix content markers mid-content and upgrade those rows to "real user" evidence after SessionDB projection strips the underscore flags, breaking fix(compression): preserve zero-user provenance in compaction summaries #69292's zero-user provenance (_is_synthetic_compression_user_turnkeys on those start-of-content markers).agent/conversation_compression.py: new_strip_stale_todo_snapshot— a snapshot merged at an earlier boundary is stripped before re-injection so repeated compactions refresh rather than accumulate todo state, and a bare stale snapshot row is refreshed in place instead of stacking a duplicate (empty/stale-skip semantics from Prevent todo snapshot injection after compression #26981; the empty-store skip itself lives inTodoStore.format_for_injection, which returnsNonefor empty or all-completed lists — now pinned by test).tests/agent/test_compression_rotation_state.py: contributor tests (merge into trailing user, multimodal tails, in-place persistence, no user/user adjacency) plus follow-up tests (scaffolding tails stay standalone with provenance verified via_transcript_has_real_user_turn, stale-row refresh, strip-before-reinject, empty-store no-op).Validation
_is_synthetic_compression_user_turnclassifies both shapes; zero-user provenance (#69292) intactTargeted tests:
tests/agent/ -k 'todo and (snapshot or inject)'→ 11 passed;tests/agent/test_context_compressor_zero_user_provenance.py→ 10 passed;tests/agent/test_compression_rotation_state.py→ 25 passed;tests/agent/test_context_compressor.py+tests/agent/test_compression_concurrent_fork.py+tests/run_agent/test_run_agent.py→ 674 passed. 0 failed.Note on the longer-term direction: #58162 by @0xLeathery (draft) proposes preserving todos as tool state (tool-call/tool-result pair) rather than any user-role row — the architecturally cleaner endgame. This PR keeps the user-visible fix shippable now without blocking that direction.
Credit
Salvaged from #53890 by @yingliang-zhang; empty-snapshot handling from #26981 by @YLChen-007; #58162 by @0xLeathery notes the longer-term tool-state direction. Fixes #26979.
Infographic