Skip to content

fix(ui): stabilize WebChat final reload reconciliation#72325

Merged
vincentkoc merged 3 commits intomainfrom
clownfish/ghcrawl-165991-agentic-merge
Apr 27, 2026
Merged

fix(ui): stabilize WebChat final reload reconciliation#72325
vincentkoc merged 3 commits intomainfrom
clownfish/ghcrawl-165991-agentic-merge

Conversation

@vincentkoc
Copy link
Copy Markdown
Member

Summary

Fixes the remaining WebChat race tracked by #66875 after the active-send flicker fix from #66997. This should keep optimistic user messages stable while also preventing assistant final/streaming output from splitting into duplicate bubbles or disappearing during deferred history reconciliation.

Credit

This follows the root-cause reports from @BiznessFish in #66875/#66274, the merged active-send fix from @scotthuang in #66997, and the broader attempted follow-up from @hansolo949 in #67037. The replacement patch should preserve those contributions while keeping the implementation narrow and addressing the dropped/deferred reload concerns raised in automated review.

Validation

  • pnpm -s vitest run ui/src/ui/app-gateway.node.test.ts ui/src/ui/app-gateway.sessions.node.test.ts ui/src/ui/chat-event-reload.test.ts ui/src/ui/controllers/chat.test.ts
  • pnpm check:changed

Notes

Keep this scoped to Control UI/WebChat event reconciliation. Do not change provider behavior, security boundaries, or unrelated history pagination.

ProjectClownfish replacement details:

@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui size: S maintainer Maintainer-authored PR labels Apr 26, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 26, 2026

Greptile Summary

This PR fixes the WebChat final-event history reconciliation race by teaching shouldReloadHistoryForFinalEvent to return false when the chat final event already carries a renderable assistant payload, and by adding a !terminalEventIsForDifferentActiveRun guard to the deferred session.message reload resolution so that a terminal event from an unrelated run cannot prematurely clear the pending key. Tests cover the key scenarios: renderable final → no reload, silent/no-payload final → reload, and deferred reload surviving unrelated terminal events.

Confidence Score: 4/5

Safe to merge; changes are narrow, well-tested, and address a documented race condition without touching unrelated code paths.

No P0/P1 findings. One P2 note about the SILENT_REPLY_PATTERN regex being case-sensitive (potential miss for lowercase/mixed-case sentinel variants). Logic is consistent across all reviewed edge cases.

chat-event-reload.ts — verify SILENT_REPLY_PATTERN case sensitivity is intentional.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/ui/chat-event-reload.ts
Line: 5

Comment:
**Case-sensitive sentinel may silently miss variants**

`SILENT_REPLY_PATTERN` only matches the exact uppercase string `NO_REPLY`. If the backend ever emits `no_reply`, `No_Reply`, or `NO_REPLY ` with a trailing newline inside the content block, `hasRenderableAssistantFinalMessage` would return `true`, suppressing the history reload and leaving the chat in a stale state. If the sentinel is contractually uppercase-only this is fine, but it is worth an explicit comment or a case-insensitive flag (`/^\s*NO_REPLY\s*$/i`) to make the intent clear.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(ui): stabilize WebChat final reload ..." | Re-trigger Greptile

Comment thread ui/src/ui/chat-event-reload.ts
@scotthuang
Copy link
Copy Markdown
Contributor

Thanks for picking this up and the kind credit! Appreciate you driving this forward systematically.

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 27, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Internal control reply tokens (ANNOUNCE_SKIP/REPLY_SKIP/no_reply) treated as renderable assistant text in UI
1. 🔵 Internal control reply tokens (ANNOUNCE_SKIP/REPLY_SKIP/no_reply) treated as renderable assistant text in UI
Property Value
Severity Low
CWE CWE-200
Location ui/src/ui/chat-event-reload.ts:5-21

Description

The UI logic that decides whether to reload chat history on a final event only treats the exact, case-sensitive token NO_REPLY as a silent/non-renderable reply.

However, the backend defines multiple internal control reply tokens (NO_REPLY, ANNOUNCE_SKIP, REPLY_SKIP) as suppressed/non-chat-visible (see src/gateway/control-reply-text.ts). With the current UI logic:

  • ANNOUNCE_SKIP / REPLY_SKIP (and lowercase variants like no_reply) are considered renderable assistant text
  • This can cause these internal protocol markers to be displayed in chat history/stream or to affect reload behavior

Impact:

  • Information disclosure of internal control/protocol markers to end users
  • Potential UI/business-logic manipulation: a final event carrying one of these tokens may suppress history reload (because it is treated as a normal assistant message), leaving the UI in an inconsistent state and potentially confusing users/auditors

Vulnerable code:

const SILENT_REPLY_PATTERN = /^\s*NO_REPLY\s*$/;
...
return typeof text === "string" && text.trim() !== "" && !SILENT_REPLY_PATTERN.test(text);

Only NO_REPLY is filtered; other known suppressed control tokens are not.

Recommendation

Treat all backend-suppressed control tokens as non-renderable in the UI.

Options:

  1. Mirror the backend token list in the UI and match case-insensitively:
const SUPPRESSED_CONTROL_TOKENS = ["NO_REPLY", "ANNOUNCE_SKIP", "REPLY_SKIP"] as const;
const SILENT_REPLY_PATTERN = new RegExp(`^\\s*(?:${SUPPRESSED_CONTROL_TOKENS.join("|")})\\s*$`, "i");
  1. Preferably, share a single source of truth (export a helper from a shared module) and use it from both the gateway and UI, e.g. a shared isSuppressedControlReplyText(text).

Also consider applying the same suppression in chat history rendering (shouldHideHistoryMessage) so these tokens never appear in transcripts shown to users.


Analyzed PR: #72325 at commit 97aba96

Last updated on: 2026-04-27T19:51:38Z

@vincentkoc
Copy link
Copy Markdown
Member Author

ProjectClownfish follow-up pushed: c362014

Fix:

  • rebased onto current main
  • resolved changelog conflict
  • addressed Aisle's UI message-integrity concern by keeping plain ANNOUNCE_SKIP/REPLY_SKIP assistant text visible; only legacy NO_REPLY remains hidden client-side

Blacksmith validation:

  • pnpm test:serial ui/src/ui/app-gateway.node.test.ts ui/src/ui/chat-event-reload.test.ts ui/src/ui/controllers/chat.test.ts covered the gateway/reload shard; controller is routed separately
  • node scripts/run-vitest.mjs run --config test/vitest/vitest.unit-ui.config.ts ui/src/ui/controllers/chat.test.ts
  • pnpm check:changed

@vincentkoc vincentkoc force-pushed the clownfish/ghcrawl-165991-agentic-merge branch from c362014 to 97aba96 Compare April 27, 2026 19:49
@vincentkoc
Copy link
Copy Markdown
Member Author

Rebased again onto current main after unrelated CI noise: 97aba96

No conflict resolution was needed on the final rebase; branch diff is still the same UI/WebChat surface. Prior Blacksmith validation remains scoped to this diff:

  • pnpm test:serial ui/src/ui/app-gateway.node.test.ts ui/src/ui/chat-event-reload.test.ts ui/src/ui/controllers/chat.test.ts for gateway/reload shard
  • node scripts/run-vitest.mjs run --config test/vitest/vitest.unit-ui.config.ts ui/src/ui/controllers/chat.test.ts
  • pnpm check:changed

@vincentkoc vincentkoc merged commit cff991c into main Apr 27, 2026
57 of 58 checks passed
@vincentkoc vincentkoc deleted the clownfish/ghcrawl-165991-agentic-merge branch April 27, 2026 19:52
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* fix(ui): stabilize WebChat final reload reconciliation

* fix(clownfish): address review for ghcrawl-165991-agentic-merge (1)

* fix(ui): keep plain control-token text visible
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
* fix(ui): stabilize WebChat final reload reconciliation

* fix(clownfish): address review for ghcrawl-165991-agentic-merge (1)

* fix(ui): keep plain control-token text visible
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui clawsweeper Tracked by ClawSweeper automation maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants