Skip to content

fix(compression): merge the todo snapshot into the trailing user turn - #69860

Merged
teknium1 merged 4 commits into
mainfrom
salvage/26981-todo-snapshot
Jul 23, 2026
Merged

fix(compression): merge the todo snapshot into the trailing user turn#69860
teknium1 merged 4 commits into
mainfrom
salvage/26981-todo-snapshot

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

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_context unconditionally 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_message probe keeps scaffolding tails (continuation marker, summary-as-user handoff, bare stale snapshot rows) on the flagged standalone path, because merging would bury the TODO_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_turn keys 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 in TodoStore.format_for_injection, which returns None for 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

Before After
Compressed transcript tail with active todos Standalone synthetic user turn carrying todo text (reads as a fresh instruction; user/user adjacency when tail is user) Snapshot folded into the trailing real user turn; alternation preserved
Tail is scaffolding (continuation marker / summary-as-user) Merge would erase synthetic classification after DB projection Flagged standalone append retained; _is_synthetic_compression_user_turn classifies both shapes; zero-user provenance (#69292) intact
Repeated compaction boundaries Snapshots accumulate Previous snapshot stripped/refreshed; exactly one snapshot block

Targeted 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

todo-snapshot-merge

yingliang-zhang and others added 4 commits July 22, 2026 21:38
… 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).
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 7eb7d0a

all good!

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/compression Context compression and continuation sessions sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 23, 2026
@teknium1
teknium1 merged commit 2ca38e5 into main Jul 23, 2026
40 checks passed
@teknium1
teknium1 deleted the salvage/26981-todo-snapshot branch July 23, 2026 14:25
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>
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 P2 Medium — degraded but workaround exists 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.

Avoid injecting raw todo snapshots as synthetic user messages after compression

3 participants