openclaw cache-TTL duplicate-emit causes self-poisoning conversation history
Version observed: openclaw 2026.5.12 (f066dd2)
Severity: High — silently degrades response quality and inflates token spend on every CLI-driven multi-turn session.
Reporter: Solarnote team, observed during routine capability testing of an agent backed by claude-haiku-4-5-20251001.
Summary
After every assistant turn, the CLI delivery path writes a second role: "assistant" message into the session transcript, immediately after firing the openclaw.cache-ttl custom event. The second message has api: "cli" and contains only the final text block (no thinking, no tool calls). It is byte-for-byte identical to the text block of the canonical assistant turn (api: "anthropic-messages").
The duplicate persists into the session JSONL and is included in the messages snapshot sent to the model on the next prompt. After a few turns, the model — seeing each of its prior responses appear twice in conversation history — starts emitting verbatim repetition within a single generation. By turn ~15 we observe 4× concatenated repetition in a single model.completed event.
Result: silent quality degradation, large token-budget overshoots, and the appearance of "the model is broken" when in fact the harness is corrupting its own context.
Reproduction
Any back-to-back CLI invocations against the same agent will reproduce. We hit it with:
openclaw agent --agent main --message "<prompt 1>" --json
openclaw agent --agent main --message "<prompt 2>" --json
# ... repeat 15+ times against the same session
The session ID is sticky across CLI invocations (default behavior), so the duplicate-emit cascade compounds across runs.
Workaround that confirms the diagnosis: passing a fresh --session-id <uuid> per invocation prevents the cascade entirely (each call starts in an empty session, so the duplicate has no prior context to interact with). See "Impact" section for quantified before/after.
Evidence (sanitised session events)
From a session JSONL emitted by openclaw 2026.5.12. The model's actual response was 8 characters. Adjacent events shown in order:
event[59] type=message
role: assistant
api: anthropic-messages
provider: anthropic
model: claude-haiku-4-5-20251001
stopReason: stop
content blocks: [thinking, text(len=8)]
event[60] type=custom
customType: openclaw.cache-ttl
data: {timestamp: ..., provider: anthropic, modelId: claude-haiku-4-5-20251001}
event[61] type=message <-- DUPLICATE
role: assistant
api: cli <-- written by the CLI delivery path
provider: anthropic
model: claude-haiku-4-5-20251001
stopReason: stop
content blocks: [text(len=8)] <-- text only, no thinking, no tool calls
usage: cost zeroed (so no double-billing), but token counts preserved
This pattern repeats for every assistant turn in the session. Verified across 30 consecutive prompts in one session — 30 of 30 assistant turns are duplicated this way.
Cascade into in-generation repetition
In the trajectory file, each individual model.completed event still has exactly one entry in data.assistantTexts. So the model is not being called twice. But the text of that single call begins to repeat itself as the conversation grows, because the model sees its own outputs duplicated in the messages snapshot:
| Test prompt (paraphrased) |
Intended response length |
Actual assistantText[0] length |
Repetitions in single generation |
| Coverage check |
595 chars |
2378 chars |
4× |
| Audit deals table |
1189 chars |
4754 chars |
4× |
| Null gallery_images |
1051 chars |
4202 chars |
4× |
Offsets are exact multiples — these are byte-for-byte verbatim concatenations, not paraphrased rewordings. The model is faithfully imitating the duplicated-self pattern it sees in its own history.
Impact
Quantified by running the same 12 tests against an openclaw agent --agent main harness with vs. without a per-test fresh --session-id workaround (model and config otherwise identical):
| Metric |
No workaround |
With workaround |
Delta |
| Avg normalised score (12 tests) |
0.638 |
0.717 |
+0.079 |
| Output tokens (12 tests, sum) |
19,092 |
4,536 |
−76% |
| Closes gap to direct-API call of same model |
— |
73% of gap closed |
— |
Per-test highlights with the bug present:
gap_001: 1001 output tokens emitted, score 0.40. With workaround: 537 tokens, 0.63.
edit_001: 5974 output tokens (6× the test's 1000-token cap) emitted by repetition, score 0.59. With workaround: 163 tokens, 0.79.
gap_004: 1404 tokens, 0.30. With workaround: 398 tokens, 0.57.
The remaining 0.028-point gap between the workaround run and a direct Anthropic API call is within scorer noise (Sonnet-as-judge is not strictly deterministic at temperature 0 across reruns).
Cost implications: the bug roughly quadruples output-token spend for any moderately long agent session. For paid models this is a direct billing impact. For prompt-cached models, the duplicate is also part of the cache write, so cache-write tokens roughly double.
Quality implications: any rubric or downstream consumer that penalises token-budget overshoot, verbose responses, or sees the response as "the model talking to itself" will mark these as failures even though the model's underlying judgement on the task is fine.
Suggested fix area
The smoking gun is in two places:
1. dist/attempt.thread-helpers-Dpqwtc2q.js
appendAttemptCacheTtlIfNeeded() (around line 309) writes the openclaw.cache-ttl custom entry. This is called from the attempt completion flow in selection-61FIEezO.js around line 9200.
2. dist/transcript-D34ZH8ZQ.js
appendAssistantMessageToSessionTranscript() (lines 445–493) writes a second assistant message into the same session transcript, marked with api: "cli", containing only [{ type: "text", text: mirrorText }]. This appears to be intended as a "delivery mirror" — the text the CLI surfaces to its caller — but it is being persisted to the same transcript that gets fed back to the model on the next turn.
The fix likely belongs in one of:
- Option A (cheapest): Don't persist the CLI/delivery mirror message to the session transcript at all. The canonical
api: "anthropic-messages" turn (event 59 above) already captures the assistant's contribution. The mirror can be returned to the CLI caller without ever touching appendExactAssistantMessageToSessionTranscript.
- Option B: Persist the mirror but exclude
api: "cli" messages when reconstructing messagesSnapshot for the next model call. Filtering by api !== "cli" in the snapshot builder would also fix it.
- Option C (only if A and B both break some other consumer): De-duplicate consecutive identical assistant turns when building the snapshot.
Option A is conceptually cleanest — the transcript is meant to be a record of what the model emitted, and the CLI mirror is a delivery artefact, not a model emission.
Notes for the maintainer
- The bug is invisible if you only look at the model.completed events (each has exactly one assistantText, so "the model is fine"), but it is glaringly visible in the session JSONL. Easy to miss in casual testing.
- It is fully reproducible with
openclaw agent --agent <any> --message <any> --json repeated 5+ times with no --session-id flag.
- We are working around it locally with: (a) fresh
uuid.uuid4() per CLI invocation, and (b) collapsing exact N-copy concatenation in the response post-processing layer. Both are documented as TODO(upstream-bug) comments in our test harness so they can be removed once this is fixed.
- The
openclaw.cache-ttl event itself is not the bug — it correctly fires once per turn. The bug is that the assistant-mirror write happens immediately afterward and lands in the same transcript.
If the maintainers would like, we are happy to share a sanitised session JSONL fragment showing the cascade across 30 turns.
openclaw cache-TTL duplicate-emit causes self-poisoning conversation history
Version observed: openclaw 2026.5.12 (f066dd2)
Severity: High — silently degrades response quality and inflates token spend on every CLI-driven multi-turn session.
Reporter: Solarnote team, observed during routine capability testing of an agent backed by
claude-haiku-4-5-20251001.Summary
After every assistant turn, the CLI delivery path writes a second
role: "assistant"message into the session transcript, immediately after firing theopenclaw.cache-ttlcustom event. The second message hasapi: "cli"and contains only the final text block (no thinking, no tool calls). It is byte-for-byte identical to the text block of the canonical assistant turn (api: "anthropic-messages").The duplicate persists into the session JSONL and is included in the messages snapshot sent to the model on the next prompt. After a few turns, the model — seeing each of its prior responses appear twice in conversation history — starts emitting verbatim repetition within a single generation. By turn ~15 we observe 4× concatenated repetition in a single
model.completedevent.Result: silent quality degradation, large token-budget overshoots, and the appearance of "the model is broken" when in fact the harness is corrupting its own context.
Reproduction
Any back-to-back CLI invocations against the same agent will reproduce. We hit it with:
The session ID is sticky across CLI invocations (default behavior), so the duplicate-emit cascade compounds across runs.
Workaround that confirms the diagnosis: passing a fresh
--session-id <uuid>per invocation prevents the cascade entirely (each call starts in an empty session, so the duplicate has no prior context to interact with). See "Impact" section for quantified before/after.Evidence (sanitised session events)
From a session JSONL emitted by openclaw 2026.5.12. The model's actual response was 8 characters. Adjacent events shown in order:
This pattern repeats for every assistant turn in the session. Verified across 30 consecutive prompts in one session — 30 of 30 assistant turns are duplicated this way.
Cascade into in-generation repetition
In the trajectory file, each individual
model.completedevent still has exactly one entry indata.assistantTexts. So the model is not being called twice. But the text of that single call begins to repeat itself as the conversation grows, because the model sees its own outputs duplicated in the messages snapshot:assistantText[0]lengthOffsets are exact multiples — these are byte-for-byte verbatim concatenations, not paraphrased rewordings. The model is faithfully imitating the duplicated-self pattern it sees in its own history.
Impact
Quantified by running the same 12 tests against an
openclaw agent --agent mainharness with vs. without a per-test fresh--session-idworkaround (model and config otherwise identical):Per-test highlights with the bug present:
gap_001: 1001 output tokens emitted, score 0.40. With workaround: 537 tokens, 0.63.edit_001: 5974 output tokens (6× the test's 1000-token cap) emitted by repetition, score 0.59. With workaround: 163 tokens, 0.79.gap_004: 1404 tokens, 0.30. With workaround: 398 tokens, 0.57.The remaining 0.028-point gap between the workaround run and a direct Anthropic API call is within scorer noise (Sonnet-as-judge is not strictly deterministic at temperature 0 across reruns).
Cost implications: the bug roughly quadruples output-token spend for any moderately long agent session. For paid models this is a direct billing impact. For prompt-cached models, the duplicate is also part of the cache write, so cache-write tokens roughly double.
Quality implications: any rubric or downstream consumer that penalises token-budget overshoot, verbose responses, or sees the response as "the model talking to itself" will mark these as failures even though the model's underlying judgement on the task is fine.
Suggested fix area
The smoking gun is in two places:
1.
dist/attempt.thread-helpers-Dpqwtc2q.jsappendAttemptCacheTtlIfNeeded()(around line 309) writes theopenclaw.cache-ttlcustom entry. This is called from the attempt completion flow inselection-61FIEezO.jsaround line 9200.2.
dist/transcript-D34ZH8ZQ.jsappendAssistantMessageToSessionTranscript()(lines 445–493) writes a second assistant message into the same session transcript, marked withapi: "cli", containing only[{ type: "text", text: mirrorText }]. This appears to be intended as a "delivery mirror" — the text the CLI surfaces to its caller — but it is being persisted to the same transcript that gets fed back to the model on the next turn.The fix likely belongs in one of:
api: "anthropic-messages"turn (event 59 above) already captures the assistant's contribution. The mirror can be returned to the CLI caller without ever touchingappendExactAssistantMessageToSessionTranscript.api: "cli"messages when reconstructingmessagesSnapshotfor the next model call. Filtering byapi !== "cli"in the snapshot builder would also fix it.Option A is conceptually cleanest — the transcript is meant to be a record of what the model emitted, and the CLI mirror is a delivery artefact, not a model emission.
Notes for the maintainer
openclaw agent --agent <any> --message <any> --jsonrepeated 5+ times with no--session-idflag.uuid.uuid4()per CLI invocation, and (b) collapsing exact N-copy concatenation in the response post-processing layer. Both are documented asTODO(upstream-bug)comments in our test harness so they can be removed once this is fixed.openclaw.cache-ttlevent itself is not the bug — it correctly fires once per turn. The bug is that the assistant-mirror write happens immediately afterward and lands in the same transcript.If the maintainers would like, we are happy to share a sanitised session JSONL fragment showing the cascade across 30 turns.