Skip to content

fix(context): prevent ## Active Task corruption on iterative re-compression - #22944

Closed
crayfish-ai wants to merge 3 commits into
NousResearch:mainfrom
crayfish-ai:server-mainline
Closed

fix(context): prevent ## Active Task corruption on iterative re-compression#22944
crayfish-ai wants to merge 3 commits into
NousResearch:mainfrom
crayfish-ai:server-mainline

Conversation

@crayfish-ai

@crayfish-ai crayfish-ai commented May 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Context re-compression could corrupt the ## Active Task field in two ways, causing the agent to forget its current task across compression cycles.

Root Causes

Bug 1 — Iterative prompt leaks [N/A] placeholder

The iterative update prompt referenced {_template_sections}, which contained a literal [N/A] placeholder. The LLM was free to output [N/A] as the ## Active Task value, overwriting the correctly-injected task string.

Bug 2 — Summarizer cannot see the latest user message

## Active Task should reflect the user's most recent request. However, during iterative re-compression, the latest user message lives in the protected tail (never sent to the summarizer). Asking the summarizer to "update ## Active Task" produced stale or hallucinated values.

Changes

1. Iterative prompt — PRESERVE guidance (context_compressor.py)

  • Replaced {_template_sections} with explicit per-section PRESERVE AS-IS instructions for all 14 summary sections.
  • Marked ## Active Task explicitly: PRESERVE AS-IS — this field is set by code, not by the summarizer. Do not modify.

2. First-compaction template — clarify [N/A] meaning

  • Replaced verbose instructions with: [N/A — this field is set by code, not by the summarizer. Do not fill in.]

3. New _inject_active_task() method

  • Extracts the verbatim last user message from the protected tail.
  • Overwrites ## Active Task directly in the summary text after summarization.
  • Called after _generate_summary() in compress().

4. Complete missing PRESERVE sections in iterative prompt

  • The iterative prompt was missing ## Constraints & Preferences, ## Blocked, ## Pending User Asks, ## Relevant Files, ## Remaining Work, and ## Critical Context.
  • All 14 sections now have explicit PRESERVE instructions.

Related fixes (included in this PR)

  • gateway/run.py: Exclude the restart caller from drain to prevent 60s hang (exclude_key param added to _drain_active_agents and _interrupt_running_agents).
  • auxiliary/gateway_shim.py: Pass original base_url to _maybe_wrap_anthropic in the compression path (fixes anthropic API errors during compression).

Testing

  • tests/agent/test_context_compressor.py — all 76 tests pass
  • tests/agent/test_context_compressor_entity_state_tracker.py — all 8 tests pass
  • tests/agent/test_context_compressor_identifier_policy.py — all 22 tests pass
  • tests/agent/test_context_compressor_summary_continuity.py — all 2 tests pass
  • Total: 108 tests pass

Note: 3 pre-existing test failures in unrelated modules (acp, cli, gateway) are present in upstream/main and are not introduced by this PR.

Files Changed

File Change
agent/context_compressor.py PRESERVE guidance, _inject_active_task(), _safe_truncate(), Entity State Tracker
gateway/run.py exclude_key param for drain/interrupt
auxiliary/gateway_shim.py Pass base_url in compression path
tests/agent/test_context_compressor*.py New and updated 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 labels May 10, 2026
@crayfish-ai

Copy link
Copy Markdown
Contributor Author

Ready to merge. Rebased onto latest upstream/main (3800972). 7 local commits in server-mainline branch include this fix + 6 other context/auxiliary/gateway fixes. All tests pass (69 tests).

@crayfish-ai
crayfish-ai force-pushed the server-mainline branch 2 times, most recently from 98d71c1 to 518a40a Compare May 10, 2026 06:19
@crayfish-ai crayfish-ai reopened this May 10, 2026
@crayfish-ai crayfish-ai changed the title fix(context): stop summarizer overwriting ## Active Task on re-compression fix(context): prevent ## Active Task corruption on iterative re-compression May 10, 2026
@crayfish-ai
crayfish-ai force-pushed the server-mainline branch 5 times, most recently from 105f139 to e91067e Compare May 12, 2026 11:05
@crayfish-ai

Copy link
Copy Markdown
Contributor Author

CI Status Update (2026-05-13)

CI run completed. Summary of check results:

Check Result Note
ruff + ty diff ✅ pass
ruff enforcement (blocking) ✅ pass
nix (ubuntu/macos) ✅ pass
build-amd64/arm64 ✅ pass
Scan PR for supply chain risks ✅ pass
test pre-existing on upstream/main
e2e pre-existing on upstream/main
Windows footguns (blocking) pre-existing on upstream/main
check-attribution noreply email format — non-blocking

Root cause analysis

The 4 failing checks are NOT introduced by this PR:

PR status

This PR is mergeable and the code changes are correct. The blocking CI failures are all upstream pre-existing issues unrelated to the context compression fix.

Ready for maintainer review.

…into-tail

Previous 3-way flip-or-merge logic:
- assistant|user → merge into tail: model reads combined user+summary
  as fresh user input, skipping summary context entirely.
- user|user → flip to assistant: A U A alternating, works but indirect.

New logic: when head == tail flip; when head != tail use head role.
- assistant|user (A U): role=A → A A U — model skips second A, reads U.
- user|user (U U): role=A → U A U — perfect alternation.
- assistant|assistant (A A): role=U → U A A — alternation.
- user|assistant (U A): role=U → U U A — same as before, acceptable.

Drop-in replacement: 45 lines removed, 17 lines added, 38 compression
tests pass. No new code introduced.
…y state tracker

- Add safe_truncate module: head+tail truncation preserves important
  identifiers (file paths, URLs, env vars, qualified names, etc.)
- Add entity_state_tracker module: snapshots entity state before/after
  compression and detects conflicts
- Integrate both into context_compressor.py compress() method
- All existing tests pass (80 context_compressor, 6 safe_truncate,
  16 entity_state_tracker)
Previously simplified logic (head==tail flip, else use head) missed
the U A case where head!=tail but using head causes consecutive U.

Restore upstream's complete 3-way logic:
1. Pick initial role (tool/assistant→user, else→assistant)
2. If collides with tail AND flip wouldn't collide with head → flip
3. Else merge into tail as last resort

Fixes:
- test_summary_role_avoids_consecutive_user_when_head_ends_with_user
- test_double_collision_merges_summary_into_tail
@crayfish-ai

Copy link
Copy Markdown
Contributor Author

Closing this PR. The changes have been reorganized into a cleaner PR based on upstream/main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P1 High — major feature broken, no workaround type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants