Skip to content

Cache-TTL handler causes duplicate assistant-turn emission, leading to in-context self-poisoning and verbatim response repetition #83427

Description

@jxrdancrane

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
Audit deals table 1189 chars 4754 chars
Null gallery_images 1051 chars 4202 chars

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions