fix(agent): align empty hidden assistant placeholders on wire to eliminate sanitizer spam (#88955) - #88996
Closed
JoaoMarcos44 wants to merge 2 commits into
Conversation
…inate sanitizer spam (NousResearch#88955)
…eam_treats_pre_existing_poisoned_row_as_no_model
teknium1
added a commit
that referenced
this pull request
Aug 18, 2026
…payload at projection time (#88955) The salvaged writer-side fix stamps api_content on NEW hidden redirect placeholders, but rows persisted before it (content="" + display_kind=hidden, no sidecar) would keep re-triggering repair_empty_non_final_messages on every call forever. Substitute [response interrupted] on the wire copy at the api_content/display_kind projection stage so legacy sessions converge too. Never the interrupt scaffold (#81841). Durable transcript untouched. Regression tests drive run_conversation end-to-end with a spied sanitizer: the projection must leave the sanitizer nothing to heal (its per-turn warning spam is the bug), verified failing via sabotage run against the writer-only fix. Projection-side approach credit: @JoaoMarcos44 (PR #88996).
teknium1
added a commit
that referenced
this pull request
Aug 18, 2026
…payload at projection time (#88955) The salvaged writer-side fix stamps api_content on NEW hidden redirect placeholders, but rows persisted before it (content="" + display_kind=hidden, no sidecar) would keep re-triggering repair_empty_non_final_messages on every call forever. Substitute [response interrupted] on the wire copy at the api_content/display_kind projection stage so legacy sessions converge too. Never the interrupt scaffold (#81841). Durable transcript untouched. Regression tests drive run_conversation end-to-end with a spied sanitizer: the projection must leave the sanitizer nothing to heal (its per-turn warning spam is the bug), verified failing via sabotage run against the writer-only fix. Projection-side approach credit: @JoaoMarcos44 (PR #88996).
Collaborator
|
Closing in favor of PR #89544, which just merged. Your PR was submitted first on this issue and your projection-side approach is what we shipped for legacy rows — the follow-up commit in #89544 credits you directly. The writer-side half came from #89525 (stamping |
lisajlau
pushed a commit
to lisajlau/hermes-agent
that referenced
this pull request
Aug 20, 2026
…payload at projection time (NousResearch#88955) The salvaged writer-side fix stamps api_content on NEW hidden redirect placeholders, but rows persisted before it (content="" + display_kind=hidden, no sidecar) would keep re-triggering repair_empty_non_final_messages on every call forever. Substitute [response interrupted] on the wire copy at the api_content/display_kind projection stage so legacy sessions converge too. Never the interrupt scaffold (NousResearch#81841). Durable transcript untouched. Regression tests drive run_conversation end-to-end with a spied sanitizer: the projection must leave the sanitizer nothing to heal (its per-turn warning spam is the bug), verified failing via sabotage run against the writer-only fix. Projection-side approach credit: @JoaoMarcos44 (PR NousResearch#88996).
bobaba76
pushed a commit
to bobaba76/hermes-agent
that referenced
this pull request
Aug 27, 2026
…payload at projection time (NousResearch#88955) The salvaged writer-side fix stamps api_content on NEW hidden redirect placeholders, but rows persisted before it (content="" + display_kind=hidden, no sidecar) would keep re-triggering repair_empty_non_final_messages on every call forever. Substitute [response interrupted] on the wire copy at the api_content/display_kind projection stage so legacy sessions converge too. Never the interrupt scaffold (NousResearch#81841). Durable transcript untouched. Regression tests drive run_conversation end-to-end with a spied sanitizer: the projection must leave the sanitizer nothing to heal (its per-turn warning spam is the bug), verified failing via sabotage run against the writer-only fix. Projection-side approach credit: @JoaoMarcos44 (PR NousResearch#88996).
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
…payload at projection time (NousResearch#88955) The salvaged writer-side fix stamps api_content on NEW hidden redirect placeholders, but rows persisted before it (content="" + display_kind=hidden, no sidecar) would keep re-triggering repair_empty_non_final_messages on every call forever. Substitute [response interrupted] on the wire copy at the api_content/display_kind projection stage so legacy sessions converge too. Never the interrupt scaffold (NousResearch#81841). Durable transcript untouched. Regression tests drive run_conversation end-to-end with a spied sanitizer: the projection must leave the sanitizer nothing to heal (its per-turn warning spam is the bug), verified failing via sabotage run against the writer-only fix. Projection-side approach credit: @JoaoMarcos44 (PR NousResearch#88996).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #88955.
In Bot Mode group chats (and active turn redirects / steering), when a member turn is interrupted before any assistant text has been streamed to the UI,
_apply_active_turn_redirect()persists an empty role placeholder withdisplay_kind="hidden":{"role": "assistant", "content": "", "display_kind": "hidden"}This placeholder is intentional and load-bearing in the session history to maintain proper role alternation (
user -> assistant -> user) without leaking unstreamed reasoning or unfinished content into user-visible transcripts.However, because this empty placeholder persisted in the session history, the pre-call sanitizer
repair_empty_non_final_messages()inagent/agent_runtime_helpers.pydetected it on every subsequent turn of the session and repeatedly emitted the following warning:In long-running group chat sessions (e.g. 98+ API turns), this warning fired perpetually on every turn, causing log pollution, unnecessary repeated memory-copy healing passes, and false-positive alarm noise.
Root Cause Analysis
The placeholder
{"role": "assistant", "content": "", "display_kind": "hidden"}is kept inmessagesso that SessionDB, transcripts, and timeline UI renderers can identify the turn as an interrupted redirect without rendering blank speech bubbles.When constructing outgoing
api_messagesinagent/conversation_loop.py,api_msg.pop("display_kind", None)stripped thedisplay_kindmetadata beforesanitize_api_messages()was invoked.repair_empty_non_final_messages()received an empty non-final assistant row without thedisplay_kind="hidden"context and treated it as unexpected data corruption rather than an intentional alternation placeholder, logging the[WARNING]every single turn.Solution
We implement a clean, two-layer wire alignment:
agent/conversation_loop.py:When constructing outgoing
api_messages, beforedisplay_kindis stripped, empty hidden assistant placeholders are proactively populated with_INTERRUPTED_PLACEHOLDER("[response interrupted]").content=""anddisplay_kind="hidden"for clean UI rendering.agent/agent_runtime_helpers.py:repair_empty_non_final_messages()distinguishes between intentional hidden placeholders and true accidental transcript corruption. It substitutes_INTERRUPTED_PLACEHOLDERcleanly while only logging warnings for unexpected non-hidden empty turns.Verification Plan
Automated Tests
tests/agent/test_interrupted_hidden_placeholder.pycovering:display_kind="hidden"placeholders when no text is visible.pytest tests/agent/test_interrupted_hidden_placeholder.py -v(5/5 PASSED)pytest tests/run_agent/test_steer.py -v(29/29 PASSED)pytest tests/run_agent/test_run_agent.py -k "scaffold or redirect or display_kind" -v(5/5 PASSED)