openclaw - 💡(How to fix) Fix Duplicate assistant.text persisted twice per turn on Gemini native streaming (v5.22) — masked by outbound dedupe, visible in session JSONL

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…

Reproducible on openclaw 2026.5.22 (a374c3a) with gemini3-native/gemini-3.5-flash (thinking off) on the native Telegram channel: every assistant text reply is persisted twice to the session JSONL, with the second record arriving 1.5–3 seconds after the first and an identical content[0].text body. The duplicate is parentId-linked to the first (not a parallel sibling).

Symptom is invisible to the end user because the v5.22 outbound dedupe ("Deduped replayed message dispatches by Telegram chat/message identity") suppresses the second dispatch — only one message appears in the Telegram client. But the duplicate persists in the session transcript and inflates token usage on the next turn.

This is adjacent to but distinct from #65924 (Moonshot retry on engine_overloaded storing both failed and successful assistant messages with duplicate tool_call IDs, closed via PR #40996 in 2026.3.22):

  • #65924 was about a retry-after-error path on a specific provider (Moonshot), and the symptom was duplicate tool_call IDs causing 400s on the next request.
  • This report is about non-retry, no-error native streaming finalization on Gemini native, where two identical assistant.text records are written with parentId linkage (not retry replacement), no provider error, no tool calls in the duplicated record.

Error Message

  • #65924 was about a retry-after-error path on a specific provider (Moonshot), and the symptom was duplicate tool_call IDs causing 400s on the next request.
  • This report is about non-retry, no-error native streaming finalization on Gemini native, where two identical assistant.text records are written with parentId linkage (not retry replacement), no provider error, no tool calls in the duplicated record.
  • No error event between the two.

Root Cause

Symptom is invisible to the end user because the v5.22 outbound dedupe ("Deduped replayed message dispatches by Telegram chat/message identity") suppresses the second dispatch — only one message appears in the Telegram client. But the duplicate persists in the session transcript and inflates token usage on the next turn.

Fix Action

Fix / Workaround

Symptom is invisible to the end user because the v5.22 outbound dedupe ("Deduped replayed message dispatches by Telegram chat/message identity") suppresses the second dispatch — only one message appears in the Telegram client. But the duplicate persists in the session transcript and inflates token usage on the next turn.

  • openclaw 2026.5.22 (a374c3a)
  • Primary model gemini3-native/gemini-3.5-flash, thinking off
  • Native Telegram channel (channels.telegram gateway-native)
  • Companion plugin extension active (registers 7 tools, 5 hooks)
  • agents.defaults.heartbeat.isolatedSession: true (workaround from #85913)
  • Single-user low-volume conversation

Hypothesis: when the v5.22 outbound dedupe suppresses the second dispatch, the first dispatch's outbound log is also silenced (perhaps the dedupe key collision sweeps both, or the logger is gated on a downstream confirmation that the dedupe short-circuits). Either way, an outbound dispatch happening (the user got the message) without any journal trace makes forensic analysis impossible.

Code Example

{"type":"message","id":"<USER_ID>","parentId":"<PREV>","timestamp":"2026-05-24T04:36:01.659Z","message":{"role":"user","content":[]}}
{"type":"message","id":"<TC1>","parentId":"<USER_ID>","timestamp":"2026-05-24T04:36:03.928Z","message":{"role":"assistant","content":[{"type":"toolCall","name":"web_search",}]}}
{"type":"message","id":"<TR1>","parentId":"<TC1>","timestamp":"2026-05-24T04:36:05.654Z","message":{"role":"toolResult","content":[]}}
{"type":"message","id":"<TC2>","parentId":"<TR1>","timestamp":"2026-05-24T04:36:07.185Z","message":{"role":"assistant","content":[{"type":"toolCall","name":"web_search",}]}}
{"type":"message","id":"<TR2>","parentId":"<TC2>","timestamp":"2026-05-24T04:36:07.588Z","message":{"role":"toolResult","content":[]}}
{"type":"message","id":"c322fc84","parentId":"<TR2>","timestamp":"2026-05-24T04:36:14.374Z","message":{"role":"assistant","content":[{"type":"text","text":"<FINAL_REPLY_1059_CHARS>"}]}}                ← first emit
{"type":"message","id":"902ee1dd-d5c2-…","parentId":"c322fc84","timestamp":"2026-05-24T04:36:17.159Z","message":{"role":"assistant","content":[{"type":"text","text":"<SAME_FINAL_REPLY_VERBATIM>"}]}}      ← duplicate 2.8s later
RAW_BUFFERClick to expand / collapse

Summary

Reproducible on openclaw 2026.5.22 (a374c3a) with gemini3-native/gemini-3.5-flash (thinking off) on the native Telegram channel: every assistant text reply is persisted twice to the session JSONL, with the second record arriving 1.5–3 seconds after the first and an identical content[0].text body. The duplicate is parentId-linked to the first (not a parallel sibling).

Symptom is invisible to the end user because the v5.22 outbound dedupe ("Deduped replayed message dispatches by Telegram chat/message identity") suppresses the second dispatch — only one message appears in the Telegram client. But the duplicate persists in the session transcript and inflates token usage on the next turn.

This is adjacent to but distinct from #65924 (Moonshot retry on engine_overloaded storing both failed and successful assistant messages with duplicate tool_call IDs, closed via PR #40996 in 2026.3.22):

  • #65924 was about a retry-after-error path on a specific provider (Moonshot), and the symptom was duplicate tool_call IDs causing 400s on the next request.
  • This report is about non-retry, no-error native streaming finalization on Gemini native, where two identical assistant.text records are written with parentId linkage (not retry replacement), no provider error, no tool calls in the duplicated record.

Environment

  • openclaw 2026.5.22 (a374c3a)
  • Primary model gemini3-native/gemini-3.5-flash, thinking off
  • Native Telegram channel (channels.telegram gateway-native)
  • Companion plugin extension active (registers 7 tools, 5 hooks)
  • agents.defaults.heartbeat.isolatedSession: true (workaround from #85913)
  • Single-user low-volume conversation

Repro — 100% on tool-heavy turns

Observed 3-of-3 user turns on a single morning (2026-05-24, 04:33–04:48 UTC). Each turn ended with a long-form text answer produced after 1–7 web_search tool calls. Each turn duplicated.

Session JSONL excerpt (Turn 2)

User asks for travel timing. Excerpt of /home/deploy/.openclaw/agents/main/sessions/<UUID>.jsonl:

{"type":"message","id":"<USER_ID>","parentId":"<PREV>","timestamp":"2026-05-24T04:36:01.659Z","message":{"role":"user","content":[…]}}
{"type":"message","id":"<TC1>","parentId":"<USER_ID>","timestamp":"2026-05-24T04:36:03.928Z","message":{"role":"assistant","content":[{"type":"toolCall","name":"web_search",…}]}}
{"type":"message","id":"<TR1>","parentId":"<TC1>","timestamp":"2026-05-24T04:36:05.654Z","message":{"role":"toolResult","content":[…]}}
{"type":"message","id":"<TC2>","parentId":"<TR1>","timestamp":"2026-05-24T04:36:07.185Z","message":{"role":"assistant","content":[{"type":"toolCall","name":"web_search",…}]}}
{"type":"message","id":"<TR2>","parentId":"<TC2>","timestamp":"2026-05-24T04:36:07.588Z","message":{"role":"toolResult","content":[…]}}
{"type":"message","id":"c322fc84","parentId":"<TR2>","timestamp":"2026-05-24T04:36:14.374Z","message":{"role":"assistant","content":[{"type":"text","text":"<FINAL_REPLY_1059_CHARS>"}]}}                ← first emit
{"type":"message","id":"902ee1dd-d5c2-…","parentId":"c322fc84","timestamp":"2026-05-24T04:36:17.159Z","message":{"role":"assistant","content":[{"type":"text","text":"<SAME_FINAL_REPLY_VERBATIM>"}]}}      ← duplicate 2.8s later

Key facts:

  • Both records have identical content[0].text verbatim (1059 chars each).
  • Second record's parentId = first record's id (not the previous toolResult). Linked chain, not parallel.
  • No error event between the two.
  • No toolCall in either; both are pure text replies.
  • No retry marker, no compaction event, no model_change between them.

All three turns same morning

TurnFirst emit (UTC)Duplicate (UTC)ΔLengthNote
104:34:29.32804:34:31.8302.5s1059 charsDuplicate carries a ↪️ Model Fallback: gemini3/gemini-3.5-flash marker — likely the only one where fallback model also produced output
204:36:14.37404:36:17.1592.8s1059 charsIdentical bodies
304:48:23.29404:48:24.8881.6s(long)Identical bodies

3-of-3 reproducibility on a quiet morning.

Logger gap that masks this

Telegram outbound send ok log line appears only for Turn 1 in both journalctl --user -u openclaw-gateway.service and /tmp/openclaw/openclaw-*.log structured JSONL. Turn 2 and Turn 3 have no outbound log event at all, yet the user clearly received both replies (confirmed via screenshots).

Hypothesis: when the v5.22 outbound dedupe suppresses the second dispatch, the first dispatch's outbound log is also silenced (perhaps the dedupe key collision sweeps both, or the logger is gated on a downstream confirmation that the dedupe short-circuits). Either way, an outbound dispatch happening (the user got the message) without any journal trace makes forensic analysis impossible.

Suggested logger fix: always emit an outbound send event with dedup_action: sent|suppressed so post-hoc analysis can distinguish "sent and the duplicate was dropped" from "never dispatched".

Hypotheses for root cause

Without gateway internals visibility, three candidates:

  1. Native streaming finalization race. Gemini native streaming emits a done chunk, gateway calls finalize() once when chunk arrives and once again on stream close. Both flush a complete assistant record.
  2. Fallback model double-completion. Turn 1 shows a Model Fallback marker on the duplicate, suggesting both primary and fallback models produced a reply and both were stored. If the trigger for fallback fires after primary already emitted, the duplicate is the fallback's output. Turn 2/3 had no visible fallback marker — perhaps fallback fires but stays unmarked when both produce identical text (Gemini deterministic on low temperature).
  3. Hook chain double-invocation. The companion plugin's before_agent_reply hook may be called twice per finalization, and if it's not idempotent on the storage side, the second invocation persists a copy.

Acceptance / fix shape

  1. Primary: Exactly one assistant.text record per user turn in session JSONL. If two finalization paths can fire, dedupe at storage layer by (parentId, content fingerprint) before append.
  2. Logger backstop: Always log outbound dispatches with dedup_action field, never silently swallow.
  3. Extend strict mode capability list (relates to #65924): strict mode currently covers Moonshot; based on this report, Gemini native should also be in the list so the existing stripConsecutiveAssistantErrors()-class sanitization covers it.

Related issues

  • #65924 — closed, Moonshot retry duplicate; this report extends class to Gemini native (no retry needed).
  • #33592, #37697 — duplicate Telegram replies (user-visible class, now masked by 5.22 outbound dedupe but root cause persists per this evidence).
  • #46005 — webchat-side duplicate user messages in JSONL (different surface, same JSONL persistence concern).
  • #66443 — overflow recovery user-message duplication (different trigger).

Impact

  • Session JSONL grows ~2x for assistant text content, inflating token usage on subsequent turns (since full history is sent to provider).
  • Forensic analysis of "did the bot reply?" requires reading session JSONL because outbound log is silenced.
  • If v5.22 outbound dedupe ever regresses or is disabled, the user-visible duplicate reply class returns.

Happy to provide full journal range, plugin manifest, or run-level CLI snapshots if useful.

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 Duplicate assistant.text persisted twice per turn on Gemini native streaming (v5.22) — masked by outbound dedupe, visible in session JSONL