fix(webchat): suppress NO_REPLY token in chat transcript rendering#32183
fix(webchat): suppress NO_REPLY token in chat transcript rendering#32183Takhoffman merged 4 commits intoopenclaw:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57b8170048
ℹ️ 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".
Greptile SummaryThis PR adds two-layer suppression of the
Confidence Score: 3/5
Last reviewed commit: 57b8170 |
57b8170 to
2521af4
Compare
There was a problem hiding this comment.
💡 Codex Review
openclaw/ui/src/ui/controllers/chat.ts
Line 259 in 2521af4
When a final event from a different run is filtered out as silent, this branch returns "final" instead of null. ui/src/ui/app-gateway.ts treats any "final" state as terminal and calls handleTerminalChatEvent(), which resets tool-stream state and flushes queued chat work, so a background NO_REPLY completion can disturb an active foreground run. The silent foreign-run path should behave like other ignored foreign-run events and return null after dropping the message.
ℹ️ 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 (text !== undefined && isSilentReplyText(text, SILENT_REPLY_TOKEN)) { | ||
| changed = true; | ||
| continue; |
There was a problem hiding this comment.
Backfill history window after filtering silent entries
This drop condition runs after chat.history has already sliced to the most recent limit transcript rows, so filtered NO_REPLY housekeeping entries are removed without replacement. In sessions where recent rows are mostly silent tokens, users can receive far fewer messages (or an empty history) even though older visible messages exist just outside the slice. The history query should backfill older rows after filtering so the requested window still represents the latest visible conversation.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. This is a valid theoretical concern but out of scope for this bugfix. In practice, NO_REPLY entries are rare — they only appear during housekeeping turns like pre-compaction memory flush. The limit: 200 history window is large enough that losing a handful of silent entries will not meaningfully reduce visible message count. Backfilling after filtering would require a loop or over-fetch strategy that adds complexity disproportionate to the risk. Tracked as a potential enhancement.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cde7ebe9f2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const text = extractText(message); | ||
| return typeof text === "string" && isSilentReplyStream(text); |
There was a problem hiding this comment.
Preserve mixed-content assistant messages in silent filter
isAssistantSilentReply() falls back to extractText(message), and extractText only aggregates text blocks while ignoring non-text blocks. That means an assistant message like content: [{type:"text",text:"NO_REPLY"}, {type:"image", ...}] is misclassified as a silent token and dropped by both loadChatHistory() and handleChatEvent(), which hides user-visible image/tool output. This is a regression introduced by the new client-side filter, especially because the gateway history path now keeps these mixed-content assistant entries.
Useful? React with 👍 / 👎.
…iring-store
- agent-components.ts: normalizeDiscordAllowList returns {allowAll, ids, names},
not an array — use ids.values().next().value instead of [0] indexing
- pairing-store.ts: add non-null assertions for stat after cache-miss guard
(resolveAllowFromReadCacheOrMissing returns early when stat is null)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Filter assistant NO_REPLY-only entries from chat.history responses at the gateway API boundary and add client-side defense-in-depth guards in the UI chat controller so internal silent tokens never render as visible chat bubbles. Two-layer fix: 1. Gateway: extractAssistantTextForSilentCheck + isSilentReplyText filter in sanitizeChatHistoryMessages (entry.text takes precedence over entry.content to avoid dropping messages with real text) 2. UI: isAssistantSilentReply + isSilentReplyStream guards on all 5 message insertion points in handleChatEvent and loadChatHistory Fixes openclaw#32015 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3ee9243 to
3015f74
Compare
|
PR #32183 - fix(webchat): suppress NO_REPLY token in chat transcript rendering (#32183) Merged and verified.
|
There was a problem hiding this comment.
💡 Codex Review
openclaw/ui/src/ui/controllers/chat.ts
Line 261 in 3015f74
When a final event arrives for a different runId and its assistant payload is filtered as silent (NO_REPLY), this branch still returns "final" instead of null. In ui/src/ui/app-gateway.ts, any "final" state triggers resetToolStream(...) and queue flushing, so a background/sibling silent run can clear tool-stream UI for the active run even though no message was appended. This regression is introduced by the new silent-filter condition and should follow the same return null behavior as other cross-run finals that don't need terminal side effects.
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…penclaw#32183) * fix(types): resolve pre-existing TS errors in agent-components and pairing-store - agent-components.ts: normalizeDiscordAllowList returns {allowAll, ids, names}, not an array — use ids.values().next().value instead of [0] indexing - pairing-store.ts: add non-null assertions for stat after cache-miss guard (resolveAllowFromReadCacheOrMissing returns early when stat is null) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(webchat): suppress NO_REPLY token in chat transcript rendering Filter assistant NO_REPLY-only entries from chat.history responses at the gateway API boundary and add client-side defense-in-depth guards in the UI chat controller so internal silent tokens never render as visible chat bubbles. Two-layer fix: 1. Gateway: extractAssistantTextForSilentCheck + isSilentReplyText filter in sanitizeChatHistoryMessages (entry.text takes precedence over entry.content to avoid dropping messages with real text) 2. UI: isAssistantSilentReply + isSilentReplyStream guards on all 5 message insertion points in handleChatEvent and loadChatHistory Fixes openclaw#32015 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(webchat): align isAssistantSilentReply text/content precedence with gateway * webchat: tighten NO_REPLY transcript and delta filtering --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…penclaw#32183) * fix(types): resolve pre-existing TS errors in agent-components and pairing-store - agent-components.ts: normalizeDiscordAllowList returns {allowAll, ids, names}, not an array — use ids.values().next().value instead of [0] indexing - pairing-store.ts: add non-null assertions for stat after cache-miss guard (resolveAllowFromReadCacheOrMissing returns early when stat is null) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(webchat): suppress NO_REPLY token in chat transcript rendering Filter assistant NO_REPLY-only entries from chat.history responses at the gateway API boundary and add client-side defense-in-depth guards in the UI chat controller so internal silent tokens never render as visible chat bubbles. Two-layer fix: 1. Gateway: extractAssistantTextForSilentCheck + isSilentReplyText filter in sanitizeChatHistoryMessages (entry.text takes precedence over entry.content to avoid dropping messages with real text) 2. UI: isAssistantSilentReply + isSilentReplyStream guards on all 5 message insertion points in handleChatEvent and loadChatHistory Fixes openclaw#32015 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(webchat): align isAssistantSilentReply text/content precedence with gateway * webchat: tighten NO_REPLY transcript and delta filtering --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…penclaw#32183) * fix(types): resolve pre-existing TS errors in agent-components and pairing-store - agent-components.ts: normalizeDiscordAllowList returns {allowAll, ids, names}, not an array — use ids.values().next().value instead of [0] indexing - pairing-store.ts: add non-null assertions for stat after cache-miss guard (resolveAllowFromReadCacheOrMissing returns early when stat is null) * fix(webchat): suppress NO_REPLY token in chat transcript rendering Filter assistant NO_REPLY-only entries from chat.history responses at the gateway API boundary and add client-side defense-in-depth guards in the UI chat controller so internal silent tokens never render as visible chat bubbles. Two-layer fix: 1. Gateway: extractAssistantTextForSilentCheck + isSilentReplyText filter in sanitizeChatHistoryMessages (entry.text takes precedence over entry.content to avoid dropping messages with real text) 2. UI: isAssistantSilentReply + isSilentReplyStream guards on all 5 message insertion points in handleChatEvent and loadChatHistory Fixes openclaw#32015 * fix(webchat): align isAssistantSilentReply text/content precedence with gateway * webchat: tighten NO_REPLY transcript and delta filtering --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Summary
Fixes #32015 — the internal sentinel token
NO_REPLYleaks into the Control UI (webchat) as a visible assistant message bubble (rendered as "NO" due to underscore truncation). This happens during silent housekeeping turns such as pre-compaction memory flush.Root cause:
sanitizeChatHistoryMessages()in the gatewaychat.historyendpoint sanitizes content blocks but never filters out assistant messages whose entire visible text isNO_REPLY. The live streaming path already suppresses these correctly viaisSilentReplyText(), but the history/reload path was unguarded.Two-layer fix:
Gateway API boundary (
src/gateway/server-methods/chat.ts):extractAssistantTextForSilentCheck()helper that extracts visible text from assistant messages, withentry.textprecedence overentry.contentto avoid dropping messages that carry real text alongside a stalecontent: "NO_REPLY"sanitizeChatHistoryMessages()from.map()tofor...ofloop that filters out assistant messages matchingisSilentReplyText()UI controller defense-in-depth (
ui/src/ui/controllers/chat.ts):isAssistantSilentReply()andisSilentReplyStream()client-side guardshandleChatEvent()andloadChatHistory()so NO_REPLY messages are never appended to the chat message listAlso fixes pre-existing CI blockers
src/discord/monitor/agent-components.ts:normalizeDiscordAllowListreturns{allowAll, ids, names}, not an array — fixed[0]indexing to useids.values().next().value(TS7053)src/pairing/pairing-store.ts: added non-null assertions forstatafter cache-miss guard whereresolveAllowFromReadCacheOrMissingguarantees non-null (TS18047)Related PRs
This is a clean, focused alternative to existing open PRs addressing the same issue. Those PRs bundle unrelated changes or only cover one layer:
This PR covers both gateway and UI layers with no unrelated changes.
Test plan
chat.history hides assistant NO_REPLY-only entries— verifies 5-message mix (user, assistant NO_REPLY dropped, assistant real kept, assistant with text="real" + content="NO_REPLY" kept, user NO_REPLY kept)tsc --noEmitandtsc -p tsconfig.plugin-sdk.dts.json— zero TS errors🤖 Generated with Claude Code