fix(tokens): catch pre-underscore prefix in silent reply detection#34733
fix(tokens): catch pre-underscore prefix in silent reply detection#34733ninjaa wants to merge 2 commits intoopenclaw:mainfrom
Conversation
…621) During streaming, 'NO' can arrive as a transient fragment before '_REPLY'. The existing isSilentReplyPrefixText() required an underscore to be present, so 'NO' slipped through and leaked to channels. Generalize the check: accept the exact pre-underscore segment of any silent token as a valid prefix (e.g. 'NO' for NO_REPLY, 'HEARTBEAT' for HEARTBEAT_OK). Fixes openclaw#32168 Closes openclaw#32403
Greptile SummaryThis PR fixes a streaming leak bug where the pre-underscore fragment of a silent reply token (e.g. Key changes:
Confidence Score: 5/5
Last reviewed commit: 26bd813 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 26bd813ea6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const underscoreIdx = tokenUpper.indexOf("_"); | ||
| if (underscoreIdx > 0 && normalized === tokenUpper.slice(0, underscoreIdx)) { | ||
| return true; |
There was a problem hiding this comment.
Preserve underscore guard for non-silent sentinel tokens
This new branch makes "HEARTBEAT" match HEARTBEAT_OK as a silent-prefix, which creates a false positive in production paths that call isSilentReplyPrefixText(text, HEARTBEAT_TOKEN) (see src/auto-reply/reply/agent-runner-execution.ts:154-155): if a model legitimately emits the uppercase word HEARTBEAT (for example when asked to output that token verbatim), the chunk is now suppressed as control text and can be dropped from user-visible output. The previous underscore guard avoided suppressing plain uppercase words for non-NO_REPLY tokens.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is intentional. For isSilentReplyPrefixText to suppress "HEARTBEAT", ALL of these must hold simultaneously:
- The model's entire accumulated output is exactly
HEARTBEAT(all caps, no spaces, no other chars) - More tokens are still streaming (if final,
isSilentReplyTexthandles it separately) - The model was producing this bare word as its complete response start
This is not a realistic production scenario. Models don't output bare HEARTBEAT as replies. Even if they did, suppression is transient — it only holds for one streaming tick until the next token arrives and breaks the prefix match (e.g., "HEARTBEAT " has a space → fails [^A-Z_] → flushes immediately).
The previous underscore guard was the root cause of the bug: it was too narrow, only catching "NO" for NO_REPLY while leaving the same leak vector open for any future tokens. The generalized approach trades an astronomically unlikely false positive for complete leak prevention across all silent token variants.
…egment
Per review: buffer N, NO, and any uppercase prefix up to the underscore
position, not just the exact pre-underscore word. This closes the edge
case where a tokenizer might emit a single char like 'N' before 'O_REPLY'.
Safe because: text must be strictly uppercase, an exact prefix of the
token, and contain only A-Z_ chars. Mixed-case ('No', 'no') is rejected.
|
Updated: Now buffers all uppercase prefixes of the token, not just the exact pre-underscore segment. How the holdback works
What gets buffered now
Why this is safeThe function requires ALL of:
A real model reply starting with a single uppercase |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d6eba6ad55
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (underscoreIdx > 0 && normalized.length <= underscoreIdx) { | ||
| return true; |
There was a problem hiding this comment.
Keep pre-underscore prefix matching scoped to NO_REPLY
This branch now marks any pre-underscore prefix as silent (HEARTBEAT, HE, etc.), which regresses non-silent token handling because normalizeStreamingText calls isSilentReplyPrefixText(text, HEARTBEAT_TOKEN) in agent-runner-execution.ts and will skip those payloads as control text. Fresh evidence: that same normalizer is also used by block reply delivery, so in block-streaming runs these suppressed chunks can be dropped from user-visible output rather than just hidden from typing signals. Please keep this relaxed pre-underscore rule limited to NO_REPLY (or an explicit allowlist) instead of applying it to all underscore tokens.
Useful? React with 👍 / 👎.
Problem
During streaming, when a model outputs
NO_REPLY(the silent reply token), the first chunk"NO"can arrive before"_REPLY". The prefix checkerisSilentReplyPrefixText()requires an underscore to be present, so"NO"alone fails the check and gets delivered to the channel as a visible message.Evidence: 9 leaked "NO" messages across multiple Mattermost channels. Full
NO_REPLYnever leaks — only"NO".Root Cause
The underscore requirement was added to avoid false positives ("N", "No" are common words), but it creates a window where the pre-underscore fragment leaks during streaming.
Fix
Generalize the check: accept the exact pre-underscore segment of any silent token as a valid prefix. For
NO_REPLY,"NO"now matches. ForHEARTBEAT_OK,"HEARTBEAT"matches. Partial segments like"N","HE","HEART"still correctly return false.4 lines changed in the function. Works for any
WORD_SUFFIXstyle token.Tests
Added T621-specific test block covering all edge cases. Updated existing generic token test. All 20 token tests pass.
Fixes #32168
Closes #32403