Bug Report: WebChat assistant text responses not persisted to session transcript (5.2 regression)
Summary
Assistant text responses delivered via WebChat are streamed to the Control UI but never written to the session JSONL transcript when the response involves an agent run (tool-using turns). On the next chat.history reload — which fires after every response completion — previous assistant text vanishes from the UI because it was never persisted. User messages and tool call/result blocks ARE persisted; only the assistant's final text content is missing.
This is a 5.2 regression. WebChat was fully functional on v2026.4.29.
Environment
- OpenClaw v2026.5.2 (8b2a6e5)
- macOS (Apple Silicon, Mac Mini M4 Pro)
- GPT-5.5 primary model
- Native install via npm (
/usr/local/lib/node_modules/openclaw/)
Steps to Reproduce
- Open Control UI WebChat on the
main session
- Send a message that triggers tool use (e.g. "what is for dinner" — triggers skill read + exec)
- Observe: Angie responds with text, visible in the chat
- Send a second message (even a simple "ping")
- Observe: the first assistant response vanishes from the UI. Only the latest response is visible.
This also reproduces with simple non-tool responses, but tool-using turns make the missing persistence most visible.
Root Cause Analysis
Two independent bugs compounding:
Bug 1: contextPruning rewrites session JSONL (config issue, FIXED locally)
agents.defaults.contextPruning.mode: "cache-ttl" with ttl: "5m" causes the gateway to rewrite the session JSONL, dropping messages older than 5 minutes. This deleted both user and assistant messages.
Fix: Set contextPruning.mode: "off" via openclaw config set. This preserved user messages.
Bug 2: WebChat agent-run path does not write assistant text to JSONL (5.2 regression, UNFIXED)
The WebChat chat.send handler in chat-DNr22c3k.js has two code paths:
-
!agentRunStarted path (lines ~1955-2048): Builds assistantContent from deliveredReplies, calls appendAssistantTranscriptMessage() to write the text to the session JSONL, then calls broadcastChatFinal(). This path works correctly — the assistant text is persisted.
-
agentRunStarted path (line 2049): When the agent run handled the response (all tool-using turns), the code falls to } else emitUserTranscriptUpdate(); — which only writes the user message transcript update. No assistant text is written. The agent run's own mirrorCodexAppServerTranscript() (in run-attempt-CektiLYp.js) writes thinking + toolCall + toolResult blocks but not the final text block.
The text IS streamed to the UI during the response (via WebSocket chat events with state: "delta"). But it is never persisted to the session JSONL. When the Control UI calls chat.history after the response completes (triggered by session.message event in oL), the history response lacks the assistant text, and the UI replaces its rendered messages with the incomplete history.
Evidence
Session JSONL after two exchanges (user→assistant, user→assistant):
line 4: user "time check?"
line 6: assistant thinking+toolCall ← no text block
line 7: toolResult
line 8: user "i like you"
line 12: assistant text "I like you too, Bobble" ← only non-tool response persisted
The tool-using response ("It's 11:52 AM EDT") was streamed to the UI but is absent from the JSONL.
Trajectory file confirms the text existed in messagesSnapshot:
{"type": "model.completed", "output": {"messagesSnapshot": [
{"role": "assistant", "content": [{"type": "text", "text": "It's 11:52 AM EDT..."}]}
]}}
The text was in the run result but never written to the session transcript.
Code References (v2026.5.2, dist files)
| File |
Line |
Description |
chat-DNr22c3k.js |
1908 |
let agentRunStarted = false; |
chat-DNr22c3k.js |
1919 |
agentRunStarted = true; (set on agent run start) |
chat-DNr22c3k.js |
1932-2048 |
if (!agentRunStarted) — transcript write + broadcastChatFinal (WORKS) |
chat-DNr22c3k.js |
2049 |
} else emitUserTranscriptUpdate(); — NO assistant text write (BROKEN) |
run-attempt-CektiLYp.js |
2041-2079 |
mirrorCodexAppServerTranscript — writes thinking+toolCall but not final text |
run-attempt-CektiLYp.js |
3242-3249 |
mirrorTranscriptBestEffort called with messagesSnapshot that has text, but mirror only writes tool-bearing messages |
transcript-C_uDP9Gl.js |
336-383 |
appendAssistantMessageToSessionTranscript — the delivery mirror (not called for webchat agent runs) |
Why 4.29 worked
On 4.29, the !agentRunStarted / agent-run split either didn't exist or the agent run path included a transcript mirror write. The 5.2 refactor separated these paths and the webchat agent-run path lost its text persistence.
Impact
- WebChat is unusable for multi-turn conversation — previous assistant responses vanish after every new message
- All channels that use the agent-run path are affected (confirmed on Telegram session too — 2/5 assistant messages had text)
- Session JSONL is structurally incomplete — downstream consumers (memory, dreaming, compaction) see tool calls without the text answer
Suggested Fix
In chat-DNr22c3k.js, the } else emitUserTranscriptUpdate(); branch (line 2049) should also extract the assistant's final text from deliveredReplies and call appendAssistantTranscriptMessage(), matching the !agentRunStarted path's behavior:
} else {
emitUserTranscriptUpdate();
const agentFinalPayloads = deliveredReplies
.filter((entry) => entry.kind === "final")
.map((entry) => entry.payload);
const agentTranscriptReply = buildTranscriptReplyText(agentFinalPayloads);
if (agentTranscriptReply) {
const { storePath: lsp, entry: le } = loadSessionEntry(sessionKey);
const sid = le?.sessionId ?? backingSessionId ?? clientRunId;
await appendAssistantTranscriptMessage({
message: agentTranscriptReply,
sessionId: sid,
storePath: lsp,
sessionFile: le?.sessionFile,
agentId,
createIfMissing: true,
cfg
});
}
}
Note: When we tested this patch locally, it wrote the text correctly but doubled the UI output (the text appeared twice — once from streaming, once from the broadcast). The duplication needs to be handled by either skipping the broadcastChatFinal for the mirrored message or deduplicating in the UI. The upstream team will know the right approach.
Related Issues
Workaround
No complete workaround exists for WebChat. Partial mitigations:
- Set
agents.defaults.contextPruning.mode: "off" to preserve user messages
- Set
gateway.webchat.chatHistoryMaxChars: 500000 to prevent text truncation
- Use Telegram or BlueBubbles as primary channel (channel delivery path includes the transcript mirror)
Bug Report: WebChat assistant text responses not persisted to session transcript (5.2 regression)
Summary
Assistant text responses delivered via WebChat are streamed to the Control UI but never written to the session JSONL transcript when the response involves an agent run (tool-using turns). On the next
chat.historyreload — which fires after every response completion — previous assistant text vanishes from the UI because it was never persisted. User messages and tool call/result blocks ARE persisted; only the assistant's final text content is missing.This is a 5.2 regression. WebChat was fully functional on v2026.4.29.
Environment
/usr/local/lib/node_modules/openclaw/)Steps to Reproduce
mainsessionThis also reproduces with simple non-tool responses, but tool-using turns make the missing persistence most visible.
Root Cause Analysis
Two independent bugs compounding:
Bug 1:
contextPruningrewrites session JSONL (config issue, FIXED locally)agents.defaults.contextPruning.mode: "cache-ttl"withttl: "5m"causes the gateway to rewrite the session JSONL, dropping messages older than 5 minutes. This deleted both user and assistant messages.Fix: Set
contextPruning.mode: "off"viaopenclaw config set. This preserved user messages.Bug 2: WebChat agent-run path does not write assistant text to JSONL (5.2 regression, UNFIXED)
The WebChat
chat.sendhandler inchat-DNr22c3k.jshas two code paths:!agentRunStartedpath (lines ~1955-2048): BuildsassistantContentfromdeliveredReplies, callsappendAssistantTranscriptMessage()to write the text to the session JSONL, then callsbroadcastChatFinal(). This path works correctly — the assistant text is persisted.agentRunStartedpath (line 2049): When the agent run handled the response (all tool-using turns), the code falls to} else emitUserTranscriptUpdate();— which only writes the user message transcript update. No assistant text is written. The agent run's ownmirrorCodexAppServerTranscript()(inrun-attempt-CektiLYp.js) writesthinking+toolCall+toolResultblocks but not the final text block.The text IS streamed to the UI during the response (via WebSocket
chatevents withstate: "delta"). But it is never persisted to the session JSONL. When the Control UI callschat.historyafter the response completes (triggered bysession.messageevent inoL), the history response lacks the assistant text, and the UI replaces its rendered messages with the incomplete history.Evidence
Session JSONL after two exchanges (user→assistant, user→assistant):
The tool-using response ("It's 11:52 AM EDT") was streamed to the UI but is absent from the JSONL.
Trajectory file confirms the text existed in
messagesSnapshot:{"type": "model.completed", "output": {"messagesSnapshot": [ {"role": "assistant", "content": [{"type": "text", "text": "It's 11:52 AM EDT..."}]} ]}}The text was in the run result but never written to the session transcript.
Code References (v2026.5.2, dist files)
chat-DNr22c3k.jslet agentRunStarted = false;chat-DNr22c3k.jsagentRunStarted = true;(set on agent run start)chat-DNr22c3k.jsif (!agentRunStarted)— transcript write + broadcastChatFinal (WORKS)chat-DNr22c3k.js} else emitUserTranscriptUpdate();— NO assistant text write (BROKEN)run-attempt-CektiLYp.jsmirrorCodexAppServerTranscript— writes thinking+toolCall but not final textrun-attempt-CektiLYp.jsmirrorTranscriptBestEffortcalled withmessagesSnapshotthat has text, but mirror only writes tool-bearing messagestranscript-C_uDP9Gl.jsappendAssistantMessageToSessionTranscript— the delivery mirror (not called for webchat agent runs)Why 4.29 worked
On 4.29, the
!agentRunStarted/ agent-run split either didn't exist or the agent run path included a transcript mirror write. The 5.2 refactor separated these paths and the webchat agent-run path lost its text persistence.Impact
Suggested Fix
In
chat-DNr22c3k.js, the} else emitUserTranscriptUpdate();branch (line 2049) should also extract the assistant's final text fromdeliveredRepliesand callappendAssistantTranscriptMessage(), matching the!agentRunStartedpath's behavior:Note: When we tested this patch locally, it wrote the text correctly but doubled the UI output (the text appeared twice — once from streaming, once from the broadcast). The duplication needs to be handled by either skipping the
broadcastChatFinalfor the mirrored message or deduplicating in the UI. The upstream team will know the right approach.Related Issues
Workaround
No complete workaround exists for WebChat. Partial mitigations:
agents.defaults.contextPruning.mode: "off"to preserve user messagesgateway.webchat.chatHistoryMaxChars: 500000to prevent text truncation