openclaw - 💡(How to fix) Fix [Bug]: WebChat CLI runner — assistant replies invisible + user messages erased after each turn [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#68611Fetched 2026-04-19 15:09:34
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0

When an agent uses a CLI provider (e.g. google-gemini-cli), the WebChat UI erases the entire conversation after every exchange: assistant replies disappear first (Bug A), and after patching that, user messages also vanish (Bug B). All other channels (TUI, Matrix/Element) work correctly.


Error Message

const emitChatFinal = (sessionKey, clientRunId, sourceRunId, seq, jobState, error, stopReason, errorKind, isCli) => { console.warn([cli-transcript] save failed: sessionKey=${sessionKey} err=${String(err)}); console.warn([cli-transcript] user msg save failed: sessionKey=${sessionKey} err=${String(err)});

Root Cause

Root Cause — Two Bugs Combined

Fix Action

Fix

Both fixes are in dist/server.impl-GQ72oJBa.js.

Code Example

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

---

const cliSessionId = resolveClaudeCliBindingSessionId(params.entry);
if (!cliSessionId) return params.localMessages;  // exits here for google-gemini-cli

---

// Change 1: signature
const emitChatFinal = (sessionKey, clientRunId, sourceRunId, seq, jobState, error, stopReason, errorKind, isCli) => {

// Change 2: inside if (jobState === "done"), before broadcast
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)}`);
    }
}

// Change 3: both call sites in finalizeLifecycleEvent
emitChatFinal(..., evt.data?.startedAt !== undefined);

---

if (update.message?.role === 'user' && update.sessionFile) {
    try {
        const { cfg: _cfg2, entry: _entry2 } = loadSessionEntry(sessionKey);
        const _aid2 = resolveSessionAgentId({ sessionKey, config: _cfg2 });
        const _ref2 = resolveSessionModelRef(_cfg2, _entry2, _aid2);
        if (isCliProvider(_ref2.provider, _cfg2))
            SessionManager.open(update.sessionFile).appendMessage(update.message);
    } catch (err) {
        console.warn(`[cli-transcript] user msg save failed: sessionKey=${sessionKey} err=${String(err)}`);
    }
}
RAW_BUFFERClick to expand / collapse

WebChat CLI Runner: Two Linked Bugs That Erase Conversation History

Version: openclaw v2026.4.15 Component: Gateway server (server.impl) / WebChat UI Severity: High — WebChat completely unusable when agent uses any CLI provider


Summary

When an agent uses a CLI provider (e.g. google-gemini-cli), the WebChat UI erases the entire conversation after every exchange: assistant replies disappear first (Bug A), and after patching that, user messages also vanish (Bug B). All other channels (TUI, Matrix/Element) work correctly.


Root Cause — Two Bugs Combined

Bug A: CLI path never persists assistant response to session transcript

In agent-runner.runtime-*.js, the CLI branch:

  • Emits stream: 'assistant' → populates server buffer
  • Emits stream: 'lifecycle', phase: 'end' → triggers emitChatFinal → broadcasts to clients
  • Does NOT write the assistant response to the session JSONL file

The embedded runner (pi-coding-agent) DOES persist internally, which is why all non-CLI paths work.

Bug B: WebChat client unconditionally reloads 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) calls chat.history from the server and overwrites chatMessages with the result.

Combined effect (Bug A + Bug B):

  • Response arrives → bp() adds it to UI → nO() calls up(e) → server returns old history (no JSONL entry for CLI response) → chatMessages overwritten → new message disappears

Bug C: User messages also missing from JSONL (revealed after fixing Bug A)

For CLI providers that are not claude-cli, augmentChatHistoryWithCliSessionImports always returns localMessages (JSONL) directly:

const cliSessionId = resolveClaudeCliBindingSessionId(params.entry);
if (!cliSessionId) return params.localMessages;  // exits here for google-gemini-cli

emitSessionTranscriptUpdate (called from emitUserTranscriptUpdate in chat.send) is a pure event emitter — its listeners only broadcast via WebSocket, they never write to disk. The JSONL write for user messages only happens inside pi-coding-agent, which is bypassed for CLI providers.

Once Bug A is fixed (JSONL now has assistant messages), up(e) reload returns assistant-only history → user messages disappear.


Reproduction

  1. Configure any agent to use a CLI provider (e.g. google-gemini-cli)
  2. Open WebChat
  3. Send a message
  4. Bug A: Reply is generated correctly (visible in logs) but never appears in WebChat
  5. After fixing Bug A: Reply appears, but user's own message disappears immediately after

Fix

Both fixes are in dist/server.impl-GQ72oJBa.js.

Fix A: Save assistant transcript before broadcast in emitChatFinal

Add isCli as 9th parameter (detected via evt.data?.startedAt !== undefined — CLI lifecycle end events include startedAt; embedded runner does not):

// Change 1: signature
const emitChatFinal = (sessionKey, clientRunId, sourceRunId, seq, jobState, error, stopReason, errorKind, isCli) => {

// Change 2: inside if (jobState === "done"), before broadcast
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)}`);
    }
}

// Change 3: both call sites in finalizeLifecycleEvent
emitChatFinal(..., evt.data?.startedAt !== undefined);

Fix B: Save user message to JSONL in createTranscriptUpdateBroadcastHandler

Insert after the sessionKey null-check:

if (update.message?.role === 'user' && update.sessionFile) {
    try {
        const { cfg: _cfg2, entry: _entry2 } = loadSessionEntry(sessionKey);
        const _aid2 = resolveSessionAgentId({ sessionKey, config: _cfg2 });
        const _ref2 = resolveSessionModelRef(_cfg2, _entry2, _aid2);
        if (isCliProvider(_ref2.provider, _cfg2))
            SessionManager.open(update.sessionFile).appendMessage(update.message);
    } catch (err) {
        console.warn(`[cli-transcript] user msg save failed: sessionKey=${sessionKey} err=${String(err)}`);
    }
}

All symbols used in both fixes are already imported/defined in server.impl-*.js.


Why the CLI discriminators are safe

Fix A — evt.data.startedAt:

  • CLI lifecycle end: { phase: "end", startedAt: X, endedAt: Y } (set by isCliProvider check at call site)
  • Embedded runner lifecycle end: { phase: "end", livenessState: "...", endedAt: Y } — no startedAt

Fix B — isCliProvider:

  • Returns true only for configured CLI backends (e.g. google-gemini-cli, claude-cli, openai-codex)
  • Returns false for embedded runner providers (openai, anthropic, etc.) → no double-write

Idempotency

  • Fix A: emitChatFinal clears chatRunState.buffers on first execution; duplicate calls have text = "", so isCli && text guard prevents double-writes
  • Fix B: emitSessionTranscriptUpdate fires at most once per chat.send (guarded by userTranscriptUpdatePromise)

Longer-term fix recommendation

The proper fix makes transcript persistence path symmetric across all runners:

  1. Make emitSessionTranscriptUpdate also write to disk (not just broadcast), or
  2. In dispatchInboundMessage.then(), the else agentRunStarted branch (CLI path) should explicitly call transcript write for both user and assistant messages, matching what the embedded runner does internally

Tested on

  • CLI provider: google-gemini-cli
  • Gateway version: v2026.4.15
  • After both fixes: WebChat ✅, Matrix/Element ✅ (no regression), TUI ✅ (no regression)
  • JSONL: alternating user + assistant entries confirmed

extent analysis

TL;DR

To fix the WebChat issue where conversation history is erased after every exchange when using a CLI provider, apply the provided fixes to server.impl-GQ72oJBa.js, specifically modifying emitChatFinal and createTranscriptUpdateBroadcastHandler to correctly save assistant and user transcripts to the session JSONL file.

Guidance

  1. Verify the issue: Confirm that the problem occurs only when using a CLI provider (e.g., google-gemini-cli) and that other channels (TUI, Matrix/Element) work correctly.
  2. Apply Fix A: Modify emitChatFinal to save the assistant transcript before broadcast by adding the isCli parameter and the necessary logic to write to the session JSONL file.
  3. Apply Fix B: Insert logic into createTranscriptUpdateBroadcastHandler to save user messages to the JSONL file when using a CLI provider.
  4. Test thoroughly: After applying both fixes, test WebChat, Matrix/Element, and TUI to ensure no regressions and that JSONL files contain alternating user and assistant entries.

Example

No additional code snippet is provided as the necessary changes are already detailed in the issue description.

Notes

  • The provided fixes are specific to the server.impl-GQ72oJBa.js file and may not apply to other versions or configurations.
  • A longer-term fix recommendation is provided, suggesting making transcript persistence symmetric across all runners.

Recommendation

Apply the provided workaround by modifying server.impl-GQ72oJBa.js as described, as this directly addresses the identified issues with saving assistant and user transcripts when using CLI providers.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug]: WebChat CLI runner — assistant replies invisible + user messages erased after each turn [1 participants]