Skip to content

feat(compaction): preserve recent user messages during compression (Codex-style) - #8529

Closed
hermes-agent-dhabibi wants to merge 1 commit into
NousResearch:mainfrom
hermes-agent-dhabibi:pr/codex-style-compaction
Closed

feat(compaction): preserve recent user messages during compression (Codex-style)#8529
hermes-agent-dhabibi wants to merge 1 commit into
NousResearch:mainfrom
hermes-agent-dhabibi:pr/codex-style-compaction

Conversation

@hermes-agent-dhabibi

Copy link
Copy Markdown
Contributor

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

@hermes-agent-dhabibi
hermes-agent-dhabibi force-pushed the pr/codex-style-compaction branch from e099d0e to 800ab9a Compare April 14, 2026 13:40
…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>
@hermes-agent-dhabibi
hermes-agent-dhabibi force-pushed the pr/codex-style-compaction branch from 800ab9a to eafdeae Compare April 14, 2026 13:58
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Apr 28, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:835 emits preserved messages as raw user turns 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() on content (agent/context_compressor.py:692 in 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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/compression Context compression and continuation sessions labels Jul 12, 2026
@teknium1

Copy link
Copy Markdown
Contributor

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 (_ensure_last_user_message_in_tail / _ensure_last_assistant_message_in_tail in agent/context_compressor.py), and every summary carries a deterministic, grounded ## Historical Task Snapshot of the latest user ask.

The verbatim multi-user-row mechanism as implemented can't land against current invariants: raw consecutive user rows violate the role-alternation contract, the 20K insert bypasses the tail token budget, .strip() crashes on multimodal list content, and the provenance machinery that has since landed (#69292/#69860 start-of-content markers, zero-user validation) treats raw user-row injection into compressed history as a hazard class.

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.

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 P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants