Wakeup-resumed turns intermittently serialize first tool call as literal <invoke> XML in text; with end_turn no retry is injected and ScheduleWakeup loops die silently

Open 💬 2 comments Opened Jun 12, 2026 by andrewstellman

Version: Claude Code 2.1.174, macOS. Models observed: Claude Opus 4.8 (the affected session). Intermittent.

Summary

I run a long-lived autonomous polling loop (a worker session that ends every turn with ScheduleWakeup, per the documented scheduled-tasks pattern). Over a single ~24h session the loop silently died 4 times (gaps of 9h15m, 2h26m, 1h22m, 2h06m), each requiring a human message to recover. Transcript forensics on the session JSONL shows the scheduler is NOT at fault — a scheduled_task_fire event is present at every scheduled time, 4/4.

The actual failure: the wakeup-resumed turn intermittently emits its FIRST tool call into the text channel as literal XML — a single text content block like:

{"type":"text","text":"court\n<invoke name=\"Bash\">\n<parameter name=\"command\">ls .../STOP 2>/dev/null && ...</parameter>..."}

(note the stray court token prefix) — instead of a structured tool_use block. No tool runs.

The outcome then depends entirely on stop_reason:

| stop_reason on the malformed turn | host behavior | outcome (observed) |
|---|---|---|
| tool_use | host injects "Your tool call was malformed and could not be parsed. Please retry." | self-heals — 4/4 occurrences recovered |
| end_turn | host sees a clean text-only completion; no retry injected | silent loop death — 4/4 occurrences killed the loop |

Same malformed-XML artifact in all 8 turns; the only difference is the stop reason. So the host already HAS the right recovery (the malformed-call retry) — it just isn't triggered when the mis-serialized call coincides with end_turn.

Repro profile (intermittent — no deterministic repro)

  • Long-lived session (~24h, ~10 multi-hour work units between idle stretches), autonomous loop via ScheduleWakeup(now + 20 min) idle ticks.
  • 8 mis-serialized first-actions out of dozens of resumed turns; 4 coincided with end_turn and died.
  • NOT correlated with compaction: the session's only compaction event postdates the last drop's recovery (0/4 drops follow one).
  • Healthy resumed ticks show first content part = tool_use, text=''.

Expected

Either (a) resumed turns never serialize tool calls into the text channel, or (b) the malformed-call retry also fires when a text-only end_turn completion contains <invoke name=-shaped content — i.e., the end_turn variant gets the same recovery the tool_use variant already gets.

Impact

Any autonomous-loop pattern built on ScheduleWakeup (the documented scheduled-tasks/worker pattern) silently halts until a human notices — in our case up to 9h15m. Idempotent disk state made recovery free, but detection was purely human.

Workarounds we use

  • External "safety tick": a cron/second-terminal process that re-enters the loop independently of the in-session turn (idempotent state machine makes redundant entry safe).
  • Candidate in-band: a Stop hook that blocks end_turn completions whose text contains <invoke name= and asks for a retry — mirrors the host's own tool_use-variant recovery.

Transcript excerpts (timestamps, event subtypes, the 8 malformed turns with stop reasons) available — extracted from ~/.claude/projects/<project>/e33217e6-*.jsonl. Happy to share sanitized excerpts.

Relationship to known issues (added after filing)

The underlying mis-serialization is the known claude-opus-4-8 boundary-token corruption family — #49747 (suspected root), #66888, #63870, #66809 (stray short-word token + literal <invoke> XML in the text channel). This report is NOT re-reporting that artifact. What it adds:

  1. The autonomous-loop death mode. When the malformed call is the FIRST action of a ScheduleWakeup-resumed turn and coincides with stop_reason: end_turn, the loop dies silently — observed gaps up to 9h15m. Prior reports in the family describe no-ops or false progress in attended sessions; an unattended loop has no human in the turn to notice.
  2. Empirical evidence for the client-side fix #63870 hypothesized. In the same session, the identical artifact with stop_reason: tool_use triggered the harness's existing "tool call was malformed, please retry" injection and self-healed 4/4 times; with end_turn it was treated as a clean text completion and died 4/4 times. So extending the existing retry to end_turn completions whose text contains <invoke name= is a complete client-side mitigation for the loop-death consequence, independent of the model-side fix.

Per #66888's cross-model data (Sonnet 4.6: zero failures), this also matches our experience: all 4 drops were an Opus 4.8 session; our Sonnet/Haiku loop sessions never exhibited it.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗