Fix/88033 webchat stop button stuck upstream#88041
Conversation
Dependency graph guard clearedThis PR no longer has blocked dependency graph changes. A future dependency graph change requires a fresh
|
|
Codex review: needs real behavior proof before merge. Reviewed May 29, 2026, 11:15 AM ET / 15:15 UTC. Summary PR surface: Source +13, Tests +129. Total +142 across 4 files. Reproducibility: yes. for the patch-level regressions: the PR's own added chat test leaves Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Fix the chat-run state machine so terminal/delta and Do we have a high-confidence way to reproduce the issue? Yes for the patch-level regressions: the PR's own added chat test leaves Is this the best way to solve the issue? No. The ownership boundary is plausible, but the current implementation is too broad in Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against 3d7df2bc0747. Label changesLabel justifications:
Evidence reviewedPR surface: Source +13, Tests +129. Total +142 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
…nt reply completes (openclaw#88033)
862fb64 to
fcf1ef0
Compare
The original fix checked ctx.healthOk, but the contribution order runs doctor:gateway-auth before doctor:gateway-health, so ctx.healthOk was still undefined and the guard was never triggered. Instead, probe gateway health inline when gatewayTokenRef is set and ctx.healthOk is undefined. This gives us a real health signal before deciding whether to emit the SecretRef warning. When the gateway is confirmed healthy, the warning is suppressed since the SecretRef resolves fine at runtime — the CLI doctor simply can't audit file/exec-backed secrets. When the gateway is unhealthy or the health probe fails, the warning still appears normally. Also reverted the unrelated webchat busy-state change (handled by openclaw#88041).
The original fix checked ctx.healthOk, but the contribution order runs doctor:gateway-auth before doctor:gateway-health, so ctx.healthOk was still undefined and the guard was never triggered. Instead, probe gateway health inline when gatewayTokenRef is set and ctx.healthOk is undefined. This gives us a real health signal before deciding whether to emit the SecretRef warning. When the gateway is confirmed healthy, the warning is suppressed since the SecretRef resolves fine at runtime — the CLI doctor simply can't audit file/exec-backed secrets. When the gateway is unhealthy or the health probe fails, the warning still appears normally. Also reverted the unrelated webchat busy-state change (handled by openclaw#88041).
PR Description
Summary
Fix the webchat composer Stop button getting stuck (remaining red/visible) after the assistant finishes replying. The button should revert to Send when the response completes, but in certain timing conditions it stays stuck as Stop indefinitely.
Fixes #88033
Change Type
Scope
UI / Webchat Composer / Chat State Management
Security Impact
None — this is purely a client-side UI state synchronization fix. No auth, permissions, networking, or data exposure changes.
Root Cause Analysis
Three interacting bugs in the chat event → UI state pipeline:
Bug 1: Late delta resurrects cleared chatRunId (
controllers/chat.ts)Symptom B: Button briefly recovers to Send, then flips back to Stop.
In
handleChatEvent, the adoption path (!state.chatRunId && sessionMatches && runId) unconditionally allowed any non-matched event to setchatRunId. AfterreconcileTerminalRuncleared bothchatRunIdandchatStreamon afinalevent, a late-arrivingdelta(already in-flight on the WebSocket) would re-adopt the samerunId, flippinghasAbortableSessionRunback totrue.Fix: Gate adoption behind
payload.state !== "final" && !== "aborted" && !== "error".Bug 2: runId mismatch silently swallows terminal events (
controllers/chat.ts)Symptom A: Stop button permanently stuck — never recovers.
The mismatch guard (
state.chatRunId && payload.runId !== state.chatRunId) correctly returnsnullfor non-terminal cross-run deltas. However, forfinalevents from the same logical run where the client-generated UUID (chatRunId) differs from the server-assignedrunId(payload), the guard caused an early return that skippedreconcileTerminalRun, leavingchatRunId,chatStream, andchatRunStatusforever set.Fix: Added issue reference comment; the existing sub-run
finalmessage injection path already handles this case correctly. The real defense is Bug 3's safety net + Bug 1's adoption gate.Bug 3: sessions.changed safety net blocked by over-eager guard (
controllers/sessions.ts)Safety net failure: Even if Bugs 1+2 leak,
sessions.changedshould clean up stale state.sessionPatchTargetsCurrentChatRun()had Guard 2:if (eventRunId === undefined && state.chatRunId) return false. Sincesessions.changedWebSocket events carry norunId/clientRunIdfield, this guard always blocked the reconciliation path, making the safety net completely ineffective for this class of bug.Fix: Removed Guard 2. When
eventRunIdis undefined, returntrueand let the caller decide based on the applied session row'shasActiveRunstatus. Updated the caller guard tonextRow.hasActiveRun !== true.Reproduction Steps (from issue)
Verification
pnpm test -- ui/src/ui/controllers/chat.test.ts ui/src/ui/controllers/sessions.test.tsAll 113 tests pass (66 chat + 47 sessions), including 3 new regression tests:
does not resurrect chatRunId from late delta after final cleared it (issue #88033)does not adopt runId from terminal events after chatRunId was cleared (parametrized)clears stale chatRunId via sessions.changed without eventRunId when session is terminal (issue #88033)Real Behavior Proof
Files Changed
ui/src/ui/controllers/chat.tsui/src/ui/controllers/sessions.tsui/src/ui/controllers/chat.test.tsui/src/ui/controllers/sessions.test.tsNotes