openclaw - 💡(How to fix) Fix [Bug]: WebChat responses invisible when agent uses CLI provider — transcript not persisted before history reload [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#68582Fetched 2026-04-19 15:09:56
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0

When an agent runs via a CLI provider (e.g. google-gemini-cli, openai-codex), responses are generated and broadcast to the WebChat UI correctly, but never actually appear — every reply disappears after a brief flash or is never shown at all. All other channels (TUI, Matrix/Element, Telegram, Signal) work correctly. Only WebChat is affected.

Error Message

console.warn([cli-transcript] save failed: sessionKey=${sessionKey} err=${String(err)});

Root Cause

Root cause (confirmed via source code)

Fix Action

Fix / Workaround

The root issue is that the CLI and embedded paths have asymmetric transcript persistence. The correct long-term fix is to make ALL execution paths (CLI and embedded) persist the assistant response to JSONL before broadcasting state: final, regardless of which runner was used. The dispatchInboundMessage callback chain is the natural place to unify this.

Code Example

if (r === 'final' && !i && jD(t)) { up(e); return; }

---

// Before the broadcast("chat", payload) call in the jobState === "done" branch:
if (isCli && text && !shouldSuppressSilent) {
    try {
        const { storePath: _sp, entry: _e } = loadSessionEntry(sessionKey);
        const _aid = resolveAgentIdFromSessionKey(sessionKey) ?? 'main';
        const _ensured = ensureSessionTranscriptFile({
            sessionId: _e?.sessionId ?? clientRunId,
            sessionFile: _e?.sessionFile,
            storePath: _sp,
            agentId: _aid
        });
        if (_ensured.ok)
            SessionManager.open(_ensured.transcriptPath).appendMessage({
                role: 'assistant',
                content: [{ type: 'text', text }],
                timestamp: Date.now(),
                stopReason: 'stop',
                usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
                         cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }
            });
    } catch (err) {
        console.warn(`[cli-transcript] save failed: sessionKey=${sessionKey} err=${String(err)}`);
    }
}
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

When an agent runs via a CLI provider (e.g. google-gemini-cli, openai-codex), responses are generated and broadcast to the WebChat UI correctly, but never actually appear — every reply disappears after a brief flash or is never shown at all. All other channels (TUI, Matrix/Element, Telegram, Signal) work correctly. Only WebChat is affected.

Steps to reproduce

  1. Configure an agent to use a CLI provider (e.g. google-gemini-cli)
  2. Open WebChat (localhost:<port>/control-ui/)
  3. Send any message
  4. Observe: CLI runs (visible in logs), response is generated, but WebChat shows nothing

Expected behavior

WebChat displays the response, consistent with TUI/Matrix behavior.

Actual behavior

WebChat shows no new message. Old messages remain visible. Every new reply disappears.

Root cause (confirmed via source code)

Two behaviors combine to produce the bug:

1. CLI path never persists the assistant response to the session JSONL transcript

In agent-runner.runtime-*.js, the CLI branch (inside if (isCliProvider(...))) emits the response via emitAgentEvent({stream:'assistant', ...}) and then emitAgentEvent({stream:'lifecycle', data:{phase:'end', startedAt:..., endedAt:...}}), which triggers emitChatFinal in createAgentEventHandler.

emitChatFinal broadcasts the response but never writes it to the session JSONL file.

The embedded runner (pi-coding-agent) DOES persist internally (before emitting the lifecycle end event), which is why all non-CLI paths work correctly.

2. WebChat client unconditionally calls chat.history after every state: final event

In control-ui/assets/index-*.js, function nO(e, t):

if (r === 'final' && !i && jD(t)) { up(e); return; }

up(e) fetches chat.history from the server and overwrites chatMessages with the result. This was presumably designed to work with the embedded runner path (where the transcript IS saved before broadcast), but breaks for the CLI path.

Combined effect: Broadcast arrives → bp() adds message to UI → nO() reloads history → server returns old JSONL (no CLI response saved) → chatMessages overwritten → message gone.

Proposed fix

File: dist/server.impl-GQ72oJBa.js (or equivalent in source)

The CLI path can be identified by the presence of evt.data.startedAt in the lifecycle end event — the CLI runner always includes it, the embedded runner does not (it uses livenessState instead). This is a stable structural discriminator.

Add transcript persistence in emitChatFinal for CLI-path runs:

// Before the broadcast("chat", payload) call in the jobState === "done" branch:
if (isCli && text && !shouldSuppressSilent) {
    try {
        const { storePath: _sp, entry: _e } = loadSessionEntry(sessionKey);
        const _aid = resolveAgentIdFromSessionKey(sessionKey) ?? 'main';
        const _ensured = ensureSessionTranscriptFile({
            sessionId: _e?.sessionId ?? clientRunId,
            sessionFile: _e?.sessionFile,
            storePath: _sp,
            agentId: _aid
        });
        if (_ensured.ok)
            SessionManager.open(_ensured.transcriptPath).appendMessage({
                role: 'assistant',
                content: [{ type: 'text', text }],
                timestamp: Date.now(),
                stopReason: 'stop',
                usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
                         cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }
            });
    } catch (err) {
        console.warn(`[cli-transcript] save failed: sessionKey=${sessionKey} err=${String(err)}`);
    }
}

Where isCli = evt.data?.startedAt !== undefined is passed from finalizeLifecycleEvent at both call sites.

All symbols (loadSessionEntry, resolveAgentIdFromSessionKey, ensureSessionTranscriptFile, SessionManager) are already imported/defined in server.impl-GQ72oJBa.js.

Idempotency: emitChatFinal deletes the buffer on first execution, so any duplicate invocation has text = "" and the guard prevents double-writes.

Longer-term recommendation

The root issue is that the CLI and embedded paths have asymmetric transcript persistence. The correct long-term fix is to make ALL execution paths (CLI and embedded) persist the assistant response to JSONL before broadcasting state: final, regardless of which runner was used. The dispatchInboundMessage callback chain is the natural place to unify this.

Version

openclaw v2026.4.15

Channels affected

  • ❌ WebChat
  • ✅ TUI (unaffected)
  • ✅ Matrix/Element (unaffected)
  • ✅ Telegram (unaffected)
  • ✅ Signal (unaffected)

extent analysis

TL;DR

The proposed fix involves adding transcript persistence in emitChatFinal for CLI-path runs by checking for the presence of evt.data.startedAt and saving the response to the session JSONL file before broadcasting.

Guidance

  • Identify if the issue is specific to the WebChat UI and CLI providers, as the problem seems to be related to the asymmetric transcript persistence between CLI and embedded paths.
  • Verify that the emitChatFinal function is not writing the response to the session JSONL file for CLI-path runs, causing the response to disappear after a brief flash.
  • Apply the proposed fix by adding the transcript persistence code in emitChatFinal for CLI-path runs, using the isCli flag to determine if the response should be saved to the session JSONL file.
  • Test the fix by running the agent via a CLI provider and checking if the response is displayed correctly in the WebChat UI.

Example

The proposed fix code snippet provided in the issue can be used as a reference:

if (isCli && text && !shouldSuppressSilent) {
    try {
        const { storePath: _sp, entry: _e } = loadSessionEntry(sessionKey);
        const _aid = resolveAgentIdFromSessionKey(sessionKey) ?? 'main';
        const _ensured = ensureSessionTranscriptFile({
            sessionId: _e?.sessionId ?? clientRunId,
            sessionFile: _e?.sessionFile,
            storePath: _sp,
            agentId: _aid
        });
        if (_ensured.ok)
            SessionManager.open(_ensured.transcriptPath).appendMessage({
                role: 'assistant',
                content: [{ type: 'text', text }],
                timestamp: Date.now(),
                stopReason: 'stop',
                usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
                         cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }
            });
    } catch (err) {
        console.warn(`[cli-transcript] save failed: sessionKey=${sessionKey} err=${String(err)}`);
    }
}

Notes

The proposed fix is specific to the openclaw v2026.4.15 version, and it's recommended to apply this fix as a temporary solution until the long-term recommendation of unifying transcript persistence across all execution paths is implemented.

Recommendation

Apply the proposed workaround by adding the transcript persistence code in emitChatFinal for CLI-path runs, as it provides a temporary solution to the issue. The long-term recommendation is to unify transcript persistence across all execution paths, but this may require further development and testing.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

WebChat displays the response, consistent with TUI/Matrix behavior.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING