Bug type
Behavior bug (incorrect output/state without crash)
Summary
Bug Report: TUI Outbound Messages Not Rendering Until Agent Response Completes
Metadata
| Field |
Value |
| Date |
2026-03-25 |
| Status |
✅ Fixed (local patch applied) |
| Severity |
Medium — UX regression; no data loss |
| Component |
openclaw-tui |
| File |
dist/tui-B38Q46Tm.js |
| Affects |
All openclaw-tui versions using this bundle |
| Reporter |
Ghost (OpenClaw internal) |
Executive Summary
When a user sends a message in the OpenClaw TUI, their outbound message briefly disappears from the chat window and does not reappear until after the agent finishes responding. The message is delivered and processed correctly — the agent receives it and replies — but the UX makes it appear as though the message was dropped or lost.
The root cause is a protocol mismatch in the TUI's "local run ID" tracking system. The TUI registers a client-generated UUID as the expected run ID for the outgoing message, but the gateway never echoes that UUID back in its chat events. This causes every incoming event to be misidentified as a remote/external run, triggering a full loadHistory() call that wipes the optimistically-rendered user message before it is painted to the terminal.
A client-side fix was applied that defers run ID registration until the gateway's actual run ID is known, resolving the rendering issue without any protocol changes.
Symptom
- User types a message and submits it in the TUI.
- The message disappears from the chat log immediately after submission.
- The agent begins processing (activity indicator shows, token count climbs).
- The user's message remains invisible throughout the entire agent response.
- Once the agent response completes, both the user's message and the agent response appear together.
The message is not lost — it reaches the gateway and the agent responds correctly. The issue is purely visual.
Reproduction Steps
- Launch
openclaw-tui and connect to a gateway with an active agent session.
- Type any message and press Enter.
- Observe: Your outbound message disappears from the chat log.
- Wait for the agent to finish responding.
- Observe: Your message reappears alongside the completed agent response.
Note: This is consistently reproducible on every message send. There is no intermittent trigger.
Root Cause Analysis
Background: Local Run ID Tracking
The TUI maintains a registry of "local run IDs" to distinguish between:
- Runs initiated by the current user — these should skip
loadHistory() on completion to avoid wiping/rebuilding the chat log.
- Runs initiated externally (other sessions, BTW runs, etc.) — these should trigger
loadHistory() to pull in changes.
The Protocol Mismatch
The tracking mechanism relies on the assumption that the run ID registered at send time will match the run ID broadcast in subsequent chat events. This assumption is incorrect:
| Step |
What Happens |
| 1 |
sendMessage() generates runId = randomUUID() (client-side UUID) |
| 2 |
runId is passed as idempotencyKey in the chat.send RPC call |
| 3 |
noteLocalRunId(runId) registers this UUID as a "local" run |
| 4 |
The gateway receives chat.send and starts a run with its own internally-generated run ID |
| 5 |
All chat events (delta, final, etc.) are broadcast with the gateway's run ID |
| 6 |
handleChatEvent calls isLocalRunId(evt.runId) — this always returns false |
| 7 |
maybeRefreshHistoryForRun() falls through to loadHistory() |
| 8 |
loadHistory() calls chatLog.clearAll() — wiping the optimistic user message |
| 9 |
History repopulates from the server after the async fetch resolves |
Key protocol fact: ChatSendParams.idempotencyKey is stored server-side for deduplication purposes only. It is never echoed back in ChatEvent.runId. The GatewayChatClient.sendChat() method returns { runId: clientUUID }, but this is the client's own UUID — not the gateway's broadcast run ID. The gateway's actual run ID is unknowable until the first chat event arrives.
Timing: Why the Message Disappears
chatLog.addUser(text) schedules a render via process.nextTick. However, loadHistory() is triggered by an incoming WebSocket event, which fires in the same or an earlier async turn. The clear (chatLog.clearAll()) races ahead of the render tick, so the terminal never paints the user message before it is erased.
Fix Applied
Approach: Replace the broken pre-registration pattern with a pendingOptimisticUserMessage flag. Defer local run ID registration until the actual gateway run ID is known from the first incoming chat event.
Change 1 — New state variable
let activeChatRunId = null;
let pendingOptimisticUserMessage = false; // ← added
let historyLoaded = false;
Change 2 — Expose via state object
get pendingOptimisticUserMessage() { return pendingOptimisticUserMessage; },
set pendingOptimisticUserMessage(value) { pendingOptimisticUserMessage = value; },
Change 3 — sendMessage(): replace broken pre-registration
// Before (broken):
chatLog.addUser(text);
noteLocalRunId(runId); // BUG: runId is client UUID, never matches gateway events
state.activeChatRunId = runId; // BUG: sets wrong ID
setActivityStatus("sending");
// After (fixed):
chatLog.addUser(text);
state.pendingOptimisticUserMessage = true; // deferred — real runId not known yet
setActivityStatus("sending");
Change 4 — handleChatEvent(): late-bind the real gateway run ID
// Before:
noteSessionRun(evt.runId);
if (!state.activeChatRunId && !isLocalBtwRunId?.(evt.runId))
state.activeChatRunId = evt.runId;
// After:
noteSessionRun(evt.runId);
if (!state.activeChatRunId && !isLocalBtwRunId?.(evt.runId)) {
state.activeChatRunId = evt.runId;
if (state.pendingOptimisticUserMessage) {
noteLocalRunId(evt.runId); // register real gateway run ID
state.pendingOptimisticUserMessage = false;
}
}
Change 5 — Clear flag on send error
if (!isBtw) state.pendingOptimisticUserMessage = false;
Why This Works
When the first chat event arrives from the gateway, we now have the real evt.runId. If a user message is pending (flag is set), we register that ID as local — preventing the subsequent loadHistory() call that was clearing the chat. The optimistic user message survives and renders correctly.
Upstream Recommendations
The client-side fix is correct and stable, but two cleaner protocol-level alternatives exist that would eliminate the need for the flag entirely:
Option A — Gateway returns its run ID in the chat.send response
Modify the chat.send RPC response to include the gateway-assigned runId. The client can then call noteLocalRunId() with the real ID immediately after sendChat() resolves, before any chat events arrive.
Pros: Eliminates timing dependency entirely. Clean and explicit.
Cons: Requires a gateway API change; minor breaking change to GatewayChatClient.sendChat() return type.
Option B — Gateway sets ChatEvent.runId to idempotencyKey when provided
When a chat.send includes an idempotencyKey, broadcast it as ChatEvent.runId for all events belonging to that run.
Pros: No client changes needed; existing pre-registration logic would work correctly.
Cons: More invasive gateway change; semantics of runId become ambiguous (client UUID vs. internal ID).
Recommendation: Option A is preferred — it's the most explicit and maintains clean separation between client identity tokens and server run IDs.
Impact Assessment
| Dimension |
Assessment |
| User impact |
High visibility — affects every single message send |
| Data integrity |
None — messages are delivered correctly |
| Workaround |
Wait for agent response before sending next message (poor UX) |
| Fix risk |
Low — isolated to rendering/state logic, no protocol changes |
| Regression surface |
Minimal — flag is cleared on both success and error paths |
Files Modified
/opt/homebrew/lib/node_modules/openclaw/dist/tui-B38Q46Tm.js — local patch applied 2026-03-25
Steps to reproduce
Steps to Reproduce
- Launch openclaw-tui and connect to a gateway with an active agent session
- Type any message in the input and press Enter
- Watch the chat log
Expected behavior
Expected Behavior
The sent message appears immediately in the chat history upon submission and remains visible while
the agent processes and responds.
Actual behavior
Actual Behavior
The sent message disappears from the chat log immediately after submission. It remains invisible
for the entire duration of the agent's response. Both the user message and the agent reply appear
together only after the agent finishes.
│ Consistently reproducible on every message send — no intermittent trigger.
OpenClaw version
OpenClaw 2026.3.23-2 (7ffe7e4)
Operating system
macOS 26.4 (Build 25E246)
Install method
No response
Model
anthropic/claude-sonnet-4-6
Provider / routing chain
anthropic/claude-sonnet-4-6 via api-key (anthropic:default)
Additional provider/model setup details
No response
Logs, screenshots, and evidence
Impact and severity
No response
Additional information
No response
Bug type
Behavior bug (incorrect output/state without crash)
Summary
Bug Report: TUI Outbound Messages Not Rendering Until Agent Response Completes
Metadata
openclaw-tuidist/tui-B38Q46Tm.jsopenclaw-tuiversions using this bundleExecutive Summary
When a user sends a message in the OpenClaw TUI, their outbound message briefly disappears from the chat window and does not reappear until after the agent finishes responding. The message is delivered and processed correctly — the agent receives it and replies — but the UX makes it appear as though the message was dropped or lost.
The root cause is a protocol mismatch in the TUI's "local run ID" tracking system. The TUI registers a client-generated UUID as the expected run ID for the outgoing message, but the gateway never echoes that UUID back in its chat events. This causes every incoming event to be misidentified as a remote/external run, triggering a full
loadHistory()call that wipes the optimistically-rendered user message before it is painted to the terminal.A client-side fix was applied that defers run ID registration until the gateway's actual run ID is known, resolving the rendering issue without any protocol changes.
Symptom
The message is not lost — it reaches the gateway and the agent responds correctly. The issue is purely visual.
Reproduction Steps
openclaw-tuiand connect to a gateway with an active agent session.Root Cause Analysis
Background: Local Run ID Tracking
The TUI maintains a registry of "local run IDs" to distinguish between:
loadHistory()on completion to avoid wiping/rebuilding the chat log.loadHistory()to pull in changes.The Protocol Mismatch
The tracking mechanism relies on the assumption that the run ID registered at send time will match the run ID broadcast in subsequent
chatevents. This assumption is incorrect:sendMessage()generatesrunId = randomUUID()(client-side UUID)runIdis passed asidempotencyKeyin thechat.sendRPC callnoteLocalRunId(runId)registers this UUID as a "local" runchat.sendand starts a run with its own internally-generated run IDchatevents (delta,final, etc.) are broadcast with the gateway's run IDhandleChatEventcallsisLocalRunId(evt.runId)— this always returnsfalsemaybeRefreshHistoryForRun()falls through toloadHistory()loadHistory()callschatLog.clearAll()— wiping the optimistic user messageKey protocol fact:
ChatSendParams.idempotencyKeyis stored server-side for deduplication purposes only. It is never echoed back inChatEvent.runId. TheGatewayChatClient.sendChat()method returns{ runId: clientUUID }, but this is the client's own UUID — not the gateway's broadcast run ID. The gateway's actual run ID is unknowable until the firstchatevent arrives.Timing: Why the Message Disappears
chatLog.addUser(text)schedules a render viaprocess.nextTick. However,loadHistory()is triggered by an incoming WebSocket event, which fires in the same or an earlier async turn. The clear (chatLog.clearAll()) races ahead of the render tick, so the terminal never paints the user message before it is erased.Fix Applied
Approach: Replace the broken pre-registration pattern with a
pendingOptimisticUserMessageflag. Defer local run ID registration until the actual gateway run ID is known from the first incomingchatevent.Change 1 — New state variable
Change 2 — Expose via state object
Change 3 —
sendMessage(): replace broken pre-registrationChange 4 —
handleChatEvent(): late-bind the real gateway run IDChange 5 — Clear flag on send error
Why This Works
When the first
chatevent arrives from the gateway, we now have the realevt.runId. If a user message is pending (flag is set), we register that ID as local — preventing the subsequentloadHistory()call that was clearing the chat. The optimistic user message survives and renders correctly.Upstream Recommendations
The client-side fix is correct and stable, but two cleaner protocol-level alternatives exist that would eliminate the need for the flag entirely:
Option A — Gateway returns its run ID in the
chat.sendresponseModify the
chat.sendRPC response to include the gateway-assignedrunId. The client can then callnoteLocalRunId()with the real ID immediately aftersendChat()resolves, before anychatevents arrive.Pros: Eliminates timing dependency entirely. Clean and explicit.
Cons: Requires a gateway API change; minor breaking change to
GatewayChatClient.sendChat()return type.Option B — Gateway sets
ChatEvent.runIdtoidempotencyKeywhen providedWhen a
chat.sendincludes anidempotencyKey, broadcast it asChatEvent.runIdfor all events belonging to that run.Pros: No client changes needed; existing pre-registration logic would work correctly.
Cons: More invasive gateway change; semantics of
runIdbecome ambiguous (client UUID vs. internal ID).Recommendation: Option A is preferred — it's the most explicit and maintains clean separation between client identity tokens and server run IDs.
Impact Assessment
Files Modified
/opt/homebrew/lib/node_modules/openclaw/dist/tui-B38Q46Tm.js— local patch applied 2026-03-25Steps to reproduce
Steps to Reproduce
Expected behavior
Expected Behavior
The sent message appears immediately in the chat history upon submission and remains visible while
the agent processes and responds.
Actual behavior
Actual Behavior
The sent message disappears from the chat log immediately after submission. It remains invisible
for the entire duration of the agent's response. Both the user message and the agent reply appear
together only after the agent finishes.
│ Consistently reproducible on every message send — no intermittent trigger.
OpenClaw version
OpenClaw 2026.3.23-2 (7ffe7e4)
Operating system
macOS 26.4 (Build 25E246)
Install method
No response
Model
anthropic/claude-sonnet-4-6
Provider / routing chain
anthropic/claude-sonnet-4-6 via api-key (anthropic:default)
Additional provider/model setup details
No response
Logs, screenshots, and evidence
Impact and severity
No response
Additional information
No response