Skip to content

fix(agent): strip fabricated 'User asked' lines from compaction summary (#62365) - #62674

Closed
Snowdchike wants to merge 2 commits into
NousResearch:mainfrom
Snowdchike:fix/compaction-fabricated-user-asks
Closed

fix(agent): strip fabricated 'User asked' lines from compaction summary (#62365)#62674
Snowdchike wants to merge 2 commits into
NousResearch:mainfrom
Snowdchike:fix/compaction-fabricated-user-asks

Conversation

@Snowdchike

Copy link
Copy Markdown

Summary

When context compaction ran on a long conversation, the summarizer LLM was pushed by the template into writing User asked: '<verbatim quote>'. When the actual transcript had no outstanding user request (or the model couldn't locate one), it fabricated a quote to fit the template — the next-turn agent then acted on a request that was never made, hallucinating a fresh task into the conversation.

Reported repro

Russian-language session where the summary header invented:

User asked: "пусть подтянет анализ кошельков"
User asked: "пришли в чат куаркод или файл"

Neither quote appeared anywhere in the source turns. The agent asked the user to clarify the wallet analysis request, the user denied ever making it, and called it a recurring bug.

Root cause

The template's wording forces a quote, so the LLM picks the closest-sounding thing in its prior to satisfy the schema instead of admitting "no outstanding ask" — which the template does allow elsewhere, but the examples strongly imply a quote is expected.

Fix

Post-validate the LLM's output against the source turns before storing it. New helper _strip_fabricated_user_asks in agent/context_compressor.py:

  1. Scan every User asked: '<quote>' line via the new _USER_ASKED_QUOTE_RE regex (matches straight / single / curly quotes, case-insensitive).
  2. For each match, normalize whitespace and case, then check substring containment in the source corpus (user/assistant/tool content + tool_call.arguments strings).
  3. If the quote isn't found in source, rewrite the line to User asked: (none — no verifiable outstanding user request in compacted turns) and log a warning. The fallback matches the existing template convention for "no outstanding task" so downstream behavior doesn't change.
  4. Trivial quotes (< 4 chars: "ok", "go", "yes") are kept as-is to avoid mangling legitimate short asks.

The verification is cheap (one regex pass + one source normalization) and gated on the actual turns the summarizer saw — no cross-thread state, no new IPC, no config flag. Single-turn correctness preserved: the existing redact_sensitive_text and strip_think_blocks post-processing still run, this is an additional pass.

Test plan

pytest tests/agent/test_compaction_fabricated_user_asks_62365.py \
       tests/agent/test_preflight_compression_gate.py -v

17 new tests + 8 preflight tests = 25/25 passed.

New test classes:

  • TestStripFabricatedUserAsks (12 tests) — verifiable kept verbatim, fabricated replaced with fallback (the [Bug]: Context compaction fabricates user requests that were never made #62365 repro), whitespace/case/curly-quote drift tolerated, non-quote prose untouched, empty source/empty summary short-circuit, multi-fabricated all stripped, mixed verifiable+fabricated, tool_call.arguments quotes detected, trivial quotes kept as-is
  • TestRegexPattern (5 tests) — locks the regex shape so a future refactor can't silently widen or narrow what counts

Files changed

  • agent/context_compressor.py — new regex + helper, single call site in _generate_summary right after redact_sensitive_text and before self._previous_summary = summary storage. The strip runs on every compaction (first-pass and iterative-update).
  • tests/agent/test_compaction_fabricated_user_asks_62365.py (new) — 17 tests pinning every clause.

Closes #62365

@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 addressing a real compaction failure mode. Current main's template does ask for the most recent unfulfilled input “verbatim” and gives User asked: examples (agent/context_compressor.py:1830-1851), so the premise is sound.

Problems

  • The new corpus accepts every role's content plus assistant tool-call arguments as evidence (agent/context_compressor.py PR lines 240-261). That cannot prove a statement labeled User asked:: an assistant-generated phrase can make a fabricated user request pass validation. The new tool-argument test (tests/agent/test_compaction_fabricated_user_asks_62365.py:156-179) currently locks in this false-positive behavior.
  • Iterative compaction retains the old summary separately but deliberately excludes it from turns_to_summarize (agent/context_compressor.py:2899-2902). The PR validates the whole updated summary only against that new-turn slice (PR line 2138), despite the iterative prompt requiring existing relevant information be preserved (agent/context_compressor.py:1908-1916). A legitimate prior active task can therefore be rewritten to none on the next compaction.

Suggested changes

  • Establish user-role-only provenance for User asked: claims.
  • Add a two-compaction regression covering preservation of a previously validated active task.

This is an automated hermes-sweeper review.

if isinstance(txt, str):
out.append(txt)
elif isinstance(content, str):
out.append(content)

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.

This appends content from every role, so an assistant or tool phrase can validate a User asked: claim even when no user made it. Restrict the evidence corpus for this assertion to user-role text; assistant-generated tool arguments are not user provenance.

Comment thread agent/context_compressor.py Outdated
# next-turn prompt carries "no outstanding ask" instead of a
# fabricated user request. Cheap (one regex pass + one source
# normalization) and gated on the turns the LLM actually saw.
summary = _strip_fabricated_user_asks(summary, turns_to_summarize)

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.

On iterative compaction, current main keeps the prior handoff in _previous_summary but excludes it from turns_to_summarize (compress() at current-main line 2902). This validates retained prior-summary asks only against new turns and can replace a legitimate old active task with none. Preserve validated prior provenance or add it to the authoritative validation source.

@alt-glitch alt-glitch added type/bug Something isn't working 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 labels Jul 11, 2026
@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 11, 2026
@Snowdchike

Copy link
Copy Markdown
Author

Addressed hermes-sweeper review:

  1. User-only provenance_serialize_user_turn_text now only accepts role == "user" content. Assistant text and tool-call args can no longer validate a User asked: claim.
  2. Iterative compaction_strip_fabricated_user_asks(..., prior_summary=self._previous_summary) preserves a previously validated active task when only new turns are re-summarized.
  3. Tests — flipped the old tool-arg false-positive case, added prior-summary preserve/strip cases. 19/19 pass.

@Snowdchike
Snowdchike force-pushed the fix/compaction-fabricated-user-asks branch from 3269cfc to ca0faa4 Compare July 11, 2026 15:52
hermes-agent and others added 2 commits July 11, 2026 22:55
…ry (#62365)

When context compaction ran on a long conversation, the summarizer LLM
was pushed by the template into writing User asked: '<verbatim
quote>'. When the actual transcript had no outstanding user request
(or the model couldn't locate one), it fabricated a quote to fit the
template — the next-turn agent then acted on a request that was never
made, hallucinating a fresh task into the conversation.

The reported repro: a Russian-language session where the summary
header invented User asked: "пусть подтянет анализ кошельков"
and User asked: "пришли в чат куаркод или файл". Neither quote
appeared anywhere in the source turns. The agent then asked the user
to clarify the wallet analysis request, the user denied ever making
it, and called it a recurring bug.

Root cause: the template's wording forces a quote, so the LLM picks
the closest-sounding thing in its prior to satisfy the schema instead
of admitting "no outstanding ask" — which the template does allow
elsewhere, but the examples strongly imply a quote is expected.

Fix: post-validate the LLM's output against the source turns before
storing it. New helper _strip_fabricated_user_asks:

  1. Scan every User asked: '<quote>' line in the LLM summary
     via the new _USER_ASKED_QUOTE_RE regex (matches straight /
     single / curly quotes, case-insensitive).
  2. For each match, normalize whitespace and case, then check
     substring containment in the source corpus (user/assistant/
     tool content + tool_call.arguments strings).
  3. If the quote isn't found in source, rewrite the line to
     User asked: (none — no verifiable outstanding user request in
     compacted turns) and log a warning. The fallback matches the
     existing template convention for "no outstanding task" so the
     shape of downstream behavior doesn't change.
  4. Trivial quotes (< 4 chars: "ok", "go", "yes") are kept as-is
     to avoid mangling legitimate short asks — the verifier is too
     brittle at that length to be worth the false-positive risk.

The verification is cheap (one regex pass + one source normalization)
and gated on the actual turns the summarizer saw — no cross-thread
state, no new IPC, no config flag. Single-turn correctness preserved:
the existing redact_sensitive_text and strip_think_blocks
post-processing still run, this is an additional pass.

Tests (17 new, in tests/agent/test_compaction_fabricated_user_asks_62365.py):

  TestStripFabricatedUserAsks (12 tests):
   - verifiable quote kept verbatim
   - fabricated quote replaced with safe fallback (the #62365 repro)
   - whitespace drift tolerated
   - case-insensitive match
   - non-quote prose User asked: left untouched
   - empty source / empty summary short-circuit cleanly
   - multiple fabricated lines all stripped
   - mixed verifiable + fabricated in same summary
   - tool_call.arguments quotes detected
   - trivial short quotes kept as-is
   - curly-quote variants recognized

  TestRegexPattern (5 tests):
   - double / single / curly quote variants all match
   - case-insensitive
   - no match without actual quoted phrase (prose safe)

Verified locally: 17/17 new + 8/8 preflight_compression_gate = 25/25.

Closes #62365
Restrict fabricated-quote validation to user-role text so assistant
or tool phrases cannot prove a claim labeled User asked. On iterative
compaction, accept the previously validated summary as provenance so a
legitimate prior active task is not rewritten to none.
teknium1 pushed a commit that referenced this pull request Jul 14, 2026
…ry (#62365)

Post-validation: scan every 'User asked:' quote the summarizer emits and
verify it appears in user-role source turns (case-insensitive, whitespace
normalized) or in the previously validated summary on iterative compaction.
Unverifiable quotes are stripped and replaced with the template's 'None.'
convention so the agent never acts on a request the user never made.

Cherry-picked from PR #62674; commit re-authored to the PR author's GitHub
identity (original commit carried a local placeholder identity).
teknium1 pushed a commit that referenced this pull request Jul 14, 2026
Assistant content and tool-call arguments are model-authored and must never
validate a claim labeled as a user request.

Cherry-picked from PR #62674 (same identity re-authoring as parent commit).
teknium1 added a commit that referenced this pull request Jul 14, 2026
@Snowdchike Snowdchike closed this Jul 15, 2026
@Snowdchike
Snowdchike deleted the fix/compaction-fabricated-user-asks branch July 15, 2026 11:11
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 P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform 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.

[Bug]: Context compaction fabricates user requests that were never made

3 participants