fix(agent): strip fabricated 'User asked' lines from compaction summary (#62365) - #62674
fix(agent): strip fabricated 'User asked' lines from compaction summary (#62365)#62674Snowdchike wants to merge 2 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
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.pyPR lines 240-261). That cannot prove a statement labeledUser 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 tononeon 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) |
There was a problem hiding this comment.
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.
| # 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) |
There was a problem hiding this comment.
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.
|
Addressed hermes-sweeper review:
|
3269cfc to
ca0faa4
Compare
…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.
…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).
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).
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:
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_asksinagent/context_compressor.py:User asked: '<quote>'line via the new_USER_ASKED_QUOTE_REregex (matches straight / single / curly quotes, case-insensitive).tool_call.argumentsstrings).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.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_textandstrip_think_blockspost-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 -v17 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-isTestRegexPattern(5 tests) — locks the regex shape so a future refactor can't silently widen or narrow what countsFiles changed
agent/context_compressor.py— new regex + helper, single call site in_generate_summaryright afterredact_sensitive_textand beforeself._previous_summary = summarystorage. 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