feat(compaction): preserve recent user messages during compression (Codex-style) - #8529
Conversation
e099d0e to
800ab9a
Compare
…odex-style) ## Enhanced Compaction (Codex-style) ### Background We studied OpenAI Codex's compact.rs to understand how they handle context compaction. Their key insight: don't just summarize — preserve the user's actual messages alongside the summary. ### The Codex Approach Codex's compaction keeps real user messages from the compressed region (up to 20K tokens, newest-first) and places them *before* the summary. This ensures the model sees user intent as the primary signal, with the summary as supplementary context. Before: [head] → [summary] → [tail] After: [head] → [preserved user messages] → [summary] → [tail] ### Changes - Add user_message_max_tokens constructor param (default 20K, matching Codex) - Configurable: set to 0 to disable user message preservation entirely - Add _collect_user_messages() — extracts user messages from the compressed region, filtering out previous compaction summaries - Modify compress() to insert preserved messages between head and summary - Add tests for new functionality Co-authored-by: dhabibi <9087935+dhabibi@users.noreply.github.com>
800ab9a to
eafdeae
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for exploring a real compaction-quality gap. Current main still summarizes the middle window without retaining its user turns verbatim (agent/context_compressor.py:2885-2935), so the idea remains relevant.
Problems
agent/context_compressor.py:835emits preserved messages as rawuserturns before the summary-role logic. Multiple preserved messages can become consecutive user roles, violating the invariant enforced by current main (agent/context_compressor.py:3063-3080;tests/agent/test_context_compressor.py:2002-2007).- The 20K preserved-message budget is inserted after tail selection, outside the token budget calculated at
agent/context_compressor.py:2860-2865; it can eliminate compaction savings before the post-assembly metric runs (agent/context_compressor.py:3141-3149). _collect_user_messages()calls.strip()oncontent(agent/context_compressor.py:692in the PR), which is unsafe for multimodal list content supported by the current assembly path.
Suggested changes
- Preserve selected text inside a single reference-only handoff or pass it to the summarizer, rather than adding raw user-role messages. The linked #499 discussion records the same concern and suggests summarizer-prompt injection.
- Rework against current compression assembly, then cover alternation, multimodal content, and bounded post-compression size.
Automated hermes-sweeper review.
| for msg in messages: | ||
| if msg.get("role") != "user": | ||
| continue | ||
| content = (msg.get("content") or "").strip() |
There was a problem hiding this comment.
content is not guaranteed to be a string: the current compressor supports multimodal list content during assembly. Calling .strip() here will raise for such a user message; normalize through the existing content helpers before filtering or truncating.
|
|
||
| # Insert preserved user messages from the summarized region. | ||
| # These come before the summary so the model sees user intent first. | ||
| for umsg in preserved_user_msgs: |
There was a problem hiding this comment.
Appending each preserved item as a raw user message bypasses the summary collision logic below. Two user turns that were originally separated by a summarized assistant turn become adjacent here, violating the strict role-alternation invariant. Preserve them in one reference-only summary/handoff payload, or include them in the role-selection logic.
|
Thanks @hermes-agent-dhabibi for surfacing Codex's compact.rs approach — the underlying insight (user intent as the primary preservation signal) is real and influenced where main ended up. The acute intent-loss case is now covered: the latest real user and assistant turns are guaranteed to anchor the protected tail ( The verbatim multi-user-row mechanism as implemented can't land against current invariants: raw consecutive If you want to pursue the surviving half (preserving N>1 user messages), the viable direction is feeding preserved user text to the summarizer input or a single reference-only handoff block — happy to review a reworked version against current assembly. Closing with credit for the design reference. |
Enhanced Compaction (Codex-style)
Background
We studied OpenAI Codex's
compact.rsto understand how they handle context compaction. Their key insight: don't just summarize — preserve the user's actual messages alongside the summary.The Codex Approach
Codex's compaction keeps real user messages from the compressed region (up to 20K tokens, newest-first) and places them before the summary. This ensures the model sees user intent as the primary signal, with the summary as supplementary context.
Before:
After:
Changes
user_message_max_tokensconstructor param (default 20K, matching Codex)_collect_user_messages()— extracts user messages from the compressed region, filtering out previous compaction summariescompress()to insert preserved messages between head and summary