fix(tui): track burst optimistic sends as a counter to prevent duplicate transcripts (NemoClaw #3145)#78969
Conversation
|
Thanks for the context here. I did a careful shell check against current Current main and the latest release already solve the central TUI duplicate/reordered optimistic-transcript problem with the merged keyed pending-row implementation from #86205, so this older counter-based branch is no longer necessary. So I’m closing this as already implemented rather than keeping a duplicate issue open. Review detailsBest possible solution: Keep the shipped keyed pending-row implementation from #86205; open a new narrow current-main report only if the burst duplicate transcript still reproduces on v2026.6.6 or later. Do we have a high-confidence way to reproduce the issue? No current-main failing reproduction was established. The PR body describes an older v2026.4.24/NemoClaw path, while current main and v2026.6.6 contain source and tests for the merged pending-row fix. Is this the best way to solve the issue? Yes for closing, not for merging this branch. The current keyed pending-row implementation is a broader and more maintainable fix than adding a counter to the old boolean path. Security review: Security review cleared: The branch only touches TUI state handling, tests, and a changelog entry; I found no dependency, workflow, secret, install, package, or code-execution surface change. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 04875efd28ee; fix evidence: release v2026.6.6, commit ac9a219692aa. |
|
Ran Pushed b045e04b as a fix-up:
Re-ran |
b045e04 to
e3d6a4c
Compare
e3d6a4c to
373bbcf
Compare
|
Rebased onto latest The previous CI run (yesterday's queue) failed on Verified locally on the new base:
Branch head: |
373bbcf to
96c1615
Compare
|
This pull request has been automatically marked as stale due to inactivity. |
|
ClawSweeper applied the proposed close for this PR.
|
Summary
openclaw tui, sending several messages back-to-back (each within ~1s) renders every user prompt and every reply twice and out of send-order. Reproduced on Linux (Ubuntu 24.04, OpenClaw 2026.4.24); reported in NemoClaw#3145.state.pendingOptimisticUserMessageis now a non-negative counter of in-flight optimistic sends instead of a boolean. Each chat send increments; each newly observed gateway run id (regardless of whether another run is already active) decrements and tags that run as locally-initiated, so every burst-sent run gets correctly bound and noloadHistory()reload races against in-flight chat events.pendingChatRunId,noteLocalRunId, and the existing single-message bind path all keep working.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
61a0b02931(preserve optimistic user messages during active runs),cc8ed8d25b(preserve user message during slow model responses).Real behavior proof (required for external PRs)
Behavior or issue addressed: TUI duplicate + reordered transcript when sending 5 messages back-to-back (NemoClaw#3145).
Real environment tested:
openclaw tui --localstartup on macOS Sonnet 14.x (Apple Silicon), agentaitsonbedrock-claude-opus-4-6, embedded gateway runtime.src/tui/tui-event-handlers.tsagainst synthesized concurrent gateway events that match the burst pattern in the issue (5 user sends, overlappingdelta/finalfrom runs 1–5).Exact steps or command run after this patch:
pnpm build(clean).pnpm openclaw tui --local --message "say hi briefly in 5 words" --timeout-ms 25000— TUI starts, embedded runtime sayslocal ready, banner readsopenclaw tui - local embedded - agent aits - session main.node --import tsx /tmp/3145-proof/burst-repro.mjsagainstorigin/main(baseline) and the patched branch. Same five-message burst, same overlapping event interleaving; the only thing that changes issrc/tui/tui-event-handlers.ts+src/tui/tui-types.ts.Evidence after fix:
Live
openclaw tui --localstartup banner (copied console output):Burst repro against the production
src/tui/tui-event-handlers.ts— copied stdout fromnode --import tsx /tmp/3145-proof/burst-repro.mjs:Patched (this PR):
Baseline (
origin/main, same script, same input, only TUI source swapped togit show origin/main:src/tui/tui-event-handlers.ts):The chatLog ops list matches because the recorder is synchronous; the runtime difference is the audit block — baseline triggers 3 stray
loadHistory()reloads on runs 2/3/5. In a liveopenclaw tui, each of those reloads callschatLog.clearAll()and rebuilds from server history, racing with livedelta/finalevents that are still arriving for the next overlapping run — that race is what produces the duplicated and reordered transcript shown in the issue.Observed result after fix: Patched build records exactly one assistant entry per run, no orphan stream reloads. The
local-tagged runsset is empty post-final becauseforgetLocalRunIdruns on each non-error final, which is the same lifecycle as the existing single-message path.activeChatRunIdreturns tonulland the counter drains to0.What was not tested: The exact Ubuntu 24.04 sandbox path described in the issue (no live
nemoclaw connectaccess). The fix is platform-agnostic — burst handling lives entirely in the TUI client, which runs the same code on every host.Before evidence: The terminal capture in NemoClaw#3145 (each prompt + reply rendered twice, send order scrambled).
Root Cause
pendingOptimisticUserMessagewas a boolean. EverysendMessageset it totrue; the first incoming gatewaychatevent consumed it (binding that runId to the optimistic send and noting it as local). When the user fired N messages before any gateway events arrived, the flag was set N times but consumed exactly once — runs 2..N were treated as external. Theirfinalevents therefore reachedmaybeRefreshHistoryForRun→loadHistory(), which callschatLog.clearAll()and rebuilds from server history. Multiple concurrent reloads racing with still-streamingdelta/finalevents produced the duplicated and reordered transcript shown in the issue. Codex review on the v1 of this patch caught a second strand: the local-binding decision was still gated on!state.activeChatRunId, so an overlapping run id (gateway runs concurrent sends per session — the burst case) skipped the bind branch entirely. The current revision moves the binding to fire on the first sighting of each non-/btwrun id, independent ofactiveChatRunId.61a0b02931,cc8ed8d25b) intentionally moved away from "tag the local runId at send time" because the gateway response runId can drift from the idempotency key under dedupe. The boolean was the pragmatic single-message bridge; this PR generalises that bridge to bursts and to the overlapping-runs case the gateway can emit.Regression Test Plan
burst-repro.mjsabove qualifies as a thin integration harness)src/tui/tui-event-handlers.test.ts—binds every burst-sent optimistic message to its gateway run id (#3145)andtags overlapping burst runs as local even while another run is active (#3145). Plussrc/tui/tui-command-handlers.test.ts—counts every burst-sent message in pendingOptimisticUserMessage (#3145).noteLocalRunId-tagged and noloadHistory()should fire on their finals — including the case where the second run id arrives mid-stream while the first run is still the active run.handleChatEventbranch that previously dropped the binding, deterministic, and matches the liveburst-repro.mjsnumbers above.binds optimistic pending messages to the first gateway run idonly locked in the single-send path.User-visible / Behavior Changes
Diagram
Security Impact
Repro + Verification
Environment
--local) and gateway-mode TUI both share this code path.aws/anthropic/bedrock-claude-opus-4-6via local agentaits.Steps
openclaw tui(oropenclaw tui --local).hi,what is 2+2,list 3 colors,what time is it,thanks).Expected
Actual (before fix)
Evidence
burst-repro.mjsoutput in the Real behavior proof section.openclaw tui --localbanner above.Human Verification
openclawruns ofburst-repro.mjsproduce the contrast above; liveopenclaw tui --local --message ...banner reacheslocal ready | idlewith the patched build./authblock path still treats counter > 0 as "pending"; overlapping run id arriving while another isactiveChatRunIdis still bound on first sighting.Review Conversations
Compatibility / Migration
pendingOptimisticUserMessageis internal TUI runtime state; the type widens frombooleantonumber, all truthiness checks (if (state.pendingOptimisticUserMessage)) keep working.Risks and Mitigations
sendChatsucceeds but the gateway never emits any chat event for that runId (e.g. gateway-side abort lost in transit).setSession/syncSessionKey) resets the counter to 0; send failure decrements; the existing streaming watchdog still resetsactiveChatRunIdand activity status, so the next user input clears any stale "sending" UI.AI-assisted
This PR was developed with assistance from Claude Code. Root-cause analysis, repro test, and patch were proposed by the agent and human-reviewed before commit.
codex review --base origin/mainwas run locally; the v1 P1 finding (overlapping-run binding gated onactiveChatRunId) was addressed in fix-up commite3d6a4c5and verified by a second clean codex review pass. Real behavior proof above is from human-runopenclawinvocations, not AI-generated.