fix: context_compressor.py - Ghost Skill P0/P1 mitigation (#32106) - #32562
fix: context_compressor.py - Ghost Skill P0/P1 mitigation (#32106)#32562dolphin-creator wants to merge 2 commits into
Conversation
|
Competing fix with #32375 for issue #32106 — both add |
|
After reviewing both PRs side by side, I believe they're complementary rather than competing. Identical on context_compressor.py — both separate skill_view from skills_list/skill_manage and add the [SKILL_PRUNED] marker. What each brings the other doesn't have:
Where this PR sits in the issue roadmap (#32106):
Proposal: I'd like to incorporate the tests from #32375 into #32562, then #32375 could be closed as superseded. The combined PR would deliver P0 + P1 with proper test coverage in a single PR. Happy to coordinate — @LeonSGP43 if you're comfortable with this approach, I'll add your tests and credit you in the commit. |
…coverage (NousResearch#32106) - TestToolResultSummaries: skill_view emits [SKILL_PRUNED], skills_list/skill_manage remain metadata-only - TestGuidanceConstants: SKILLS_GUIDANCE includes ## Skill Safety Rule with reload instruction - Credits: test patterns from LeonSGP43 (PR NousResearch#32375), adapted for merged PR
|
Tests added (commit b7aaf2a) — this PR now covers both P0 and P1 with test coverage:
Test patterns adapted from @LeonSGP43 PR #32375 with credit in the commit. All 4 pass. Proposal: Since this PR now includes the tests that were unique to #32375, plus the @alt-glitch happy to coordinate on the merge if this looks good. |
|
Production validation: I've been running these exact P0+P1 patches on my own Hermes Agent instance since May 26th (3 days in production). Zero issues, zero ghost skill loops, zero crashes. The Happy to provide more data points if needed. |
…ompression Complements PR NousResearch#32562 (P0/P1 Ghost Skill mitigation). Pre-pass v2: - New _prune_stale_skill_views() runs BEFORE _prune_old_tool_results - Heuristic: single-use skills pruned, reused/recent skills protected - Early return if pruning brings tokens below threshold (saves LLM call) Summary P2: - Extract [SKILL_PRUNED] markers before _serialize_for_summary() - Add '## Pruned Skills' section to summary template with verbatim directive - Post-LLM: re-inject markers if summarizer paraphrased them away - Fixes NousResearch#32106: LLM summary was diluting [SKILL_PRUNED] into vague prose
…ompression Complements PR NousResearch#32562 (P0/P1 Ghost Skill mitigation). Pre-pass v2: - New _prune_stale_skill_views() runs BEFORE _prune_old_tool_results - Heuristic: single-use skills pruned, reused/recent skills protected - Early return if pruning brings tokens below threshold (saves LLM call) Summary P2: - Extract [SKILL_PRUNED] markers before _serialize_for_summary() - Add '## Pruned Skills' section to summary template with verbatim directive - Post-LLM: re-inject markers if summarizer paraphrased them away - Fixes NousResearch#32106: LLM summary was diluting [SKILL_PRUNED] into vague prose
|
Thanks for the focused P0/P1 proposal and for incorporating the targeted Problems
Suggested changes
This is an automated hermes-sweeper review. |
|
Thanks @dolphin-creator — this was the first focused P0/P1 implementation of the ghost-skill fix (#32106), with the correct Closing as consolidated into your own #44166, which is a strict superset (same P0 marker branch + Skill Safety Rule, plus the P2 marker-survival layer that answers the "marker is only summarizer input with no survival guarantee" objection). Keeping one vehicle per mechanism — #44166 is the live one. (One note for the rework there: this branch's |
Fix: Ghost Skill Syndrome — Prevent context compression from silently losing loaded skills
Summary
Context compression prunes loaded skill content from conversation history, leaving the agent with stale
[SKILL_PRUNED]placeholders it can't distinguish from valid skill content. This causes infinite loops, hallucinated instructions, and wasted tokens in every long-running session that uses skills.This PR introduces a 3-layer defense system (P0 pre-pass, P1 system prompt rule, P2 summary preservation) that ensures the agent always knows when a skill's content has been lost and can reload it instead of hallucinating.
The Problem
What happens today (broken)
Real-world symptoms:
skill_view → [SKILL_PRUNED] → skill_view → [SKILL_PRUNED](same skill, same result, infinite)[SKILL_PRUNED]markers into vague prose like "some skills were loaded" — losing the signal entirelyAffected: Every long-running session using skills. The longer the session, the worse the degradation.
The Solution
Three layers of defense
skill_viewresults before the summarizer runs, replacing them with structured[SKILL_PRUNED]markers that carry the skill name + reload instructioncontext_compressor._prune_stale_skill_views()[SKILL_PRUNED]means: reload withskill_view(), don't hallucinate, and dedup after reloadingprompt_builder.SKILLS_GUIDANCE[SKILL_PRUNED]markers before LLM summarization and re-injects them after, since the LLM paraphrases them awaycontext_compressor._generate_summary()Layer 1 — P0: Pre-pass v2 (pre-compression pruning)
A dedicated
_prune_stale_skill_views()pass runs before general tool output pruning, specifically targetingskill_viewresults that are no longer relevant:Heuristic (no LLM needed, zero cost):
When a skill is pruned, its content is replaced with a structured placeholder:
This placeholder:
tool_callenvelope so message history stays coherentEarly exit: If pruning stale skills brings the token count below the compression threshold, the entire LLM summarization step is skipped — saving the aux-model API call entirely.
Layer 2 — P1: Skill Safety Rule (system prompt)
The system prompt now includes explicit instructions about
[SKILL_PRUNED]markers:This prevents the agent from:
[SKILL_PRUNED]as valid skill content and hallucinating from itLayer 3 — P2: Summary preservation
The LLM summarizer receives the entire compressed middle as text — including our
[SKILL_PRUNED]markers. Without protection, it paraphrases them into vague prose like "some skills were loaded earlier."Two-pronged fix:
Template directive: The summary template includes a
## Pruned Skillssection with explicit instructions:Defensive re-injection: After the LLM returns the summary, we check if
[SKILL_PRUNED]survived. If the LLM paraphrased it away, we append the canonical markers back:This guarantees the marker survives every compaction cycle, no matter how aggressively the LLM paraphrases.
Files Changed
agent/context_compressor.py_prune_stale_skill_views), P2 extraction/re-injection, early-exit optimization, dedup noteagent/prompt_builder.pyTesting
Manual testing scenarios
[SKILL_PRUNED]marker preserved, agent reloads skill on next use[SKILL_PRUNED]paraphrased away after 2nd compactionLog output (healthy session)
Migration / Backward Compatibility
[SKILL_PRUNED]markers from older compressions are recognized by the P1 system prompt rule._prune_old_tool_results.Related Issues
[SKILL_PRUNED]markers across compaction cycles