Skip to content

WebChat: assistant text responses not persisted to session transcript (5.2 regression) #76804

@dertbv

Description

@dertbv

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

  1. Open Control UI WebChat on the main session
  2. Send a message that triggers tool use (e.g. "what is for dinner" — triggers skill read + exec)
  3. Observe: Angie responds with text, visible in the chat
  4. Send a second message (even a simple "ping")
  5. 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:

  1. !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.

  2. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions