Skip to content

[Bug]: deliveryMode="final_only" silently drops long tool-loop replies when no phase=final chunk is emitted #76828

@sene1337

Description

@sene1337

Summary

On OpenClaw 2026.5.2 (8b2a6e5) with acp.stream.deliveryMode = "final_only" and a Telegram-backed agent session, when an agent turn includes a long tool loop (8+ tool calls, ~15+ minutes of work) the assistant emits a sequence of legitimate progress-narration TEXT chunks — but every chunk has textSignature.phase = "commentary". No chunk in the entire turn ever gets marked phase = "final". Under final_only, the channel delivery layer silently strips all of them. The user sees: typing indicator on/off, then total silence, while the agent has actually done correct work and saved correct status messages to the session JSONL.

This appears to be a regression vs. earlier OpenClaw behavior where the runtime would either (a) promote the last commentary chunk to phase=final on turn-end, or (b) emit an explicit phase=final wrap-up itself.

Environment

  • OpenClaw: 2026.5.2 (8b2a6e5)
  • Runtime: Node v22.22.0
  • OS: Darwin 25.4.0 arm64
  • Channel: Telegram group, topic 1 (agent:main:telegram:group:<chat_id>:topic:1)
  • Provider/model: openai-codex/gpt-5.5
  • ACP stream config: {coalesceIdleMs: 300, maxChunkChars: 1200, deliveryMode: "final_only"}

Reproduction

  1. Run OpenClaw 2026.5.2 with acp.stream.deliveryMode = "final_only", an agent configured for a Telegram channel, and a session that has at least one prior turn so context isn't trivial.
  2. Send the agent a request that legitimately requires many tool calls — e.g., "clean up the plugin migration warnings" (forces doctor → config edit → reload → doctor → secrets reload → verification, 8+ tool calls).
  3. Observe in ~/.openclaw/agents/main/sessions/<sessionId>.jsonl: the agent emits multiple assistant TEXT chunks during the turn, each saved with textSignature containing "phase":"commentary".
  4. Observe in ~/.openclaw/logs/gateway.log: zero [telegram] sendMessage ok chat=<id> lines for the affected chat for the duration of the turn.
  5. Observe on Telegram: nothing arrives, even after the runtime nudge [OpenClaw heartbeat poll] Continue the OpenClaw runtime event fires and the agent acknowledges and continues.
  6. Eventually the agent produces a final phase=commentary chunk that internally signals "done" via tone (e.g., "Verifying clean boot now…" or "Restarting once now to load the cleaned config…") but never explicitly marks phase=final. Nothing delivers.

Concrete evidence — the operator's session today

User asked at 2026-05-03T16:13:59 UTC (12:13 EDT) in topic 1: "can you fix the other plugin errors".

Across the next 17 minutes the agent emitted these assistant TEXT chunks (extracted from the session JSONL with phase from textSignature):

ts (UTC) phase text excerpt
16:13:59 commentary "Got it. I'll start with the actual plugin load surface first…"
16:14:45 commentary "I found three distinct buckets already: stale config refs diffs, acpx, bluebubbles…"
16:15:10 commentary "The gateway config tool refused this as 'protected config paths,' so I'm switching to the safe fallback…"
16:25:13 commentary "Continuing. I've got enough context from the event and active task file…"
16:25:31 commentary "Yes — I found real signal. Stale plugin refs are cleaned from config…"
16:27:55 commentary "Short version: two fixes are done, one thing remains…"
16:28:51 commentary "Verifying clean boot now: plugin doctor, full doctor, then the latest xAI and lossless warnings…"

Telegram delivery to the same chat in the same window:

$ grep "sendMessage.*chat=<chat_id>" ~/.openclaw/logs/gateway.log
2026-05-03T10:50:57.713-04:00 [telegram] sendMessage ok chat=<chat_id> message=4069
2026-05-03T10:58:48.021-04:00 [telegram] sendMessage ok chat=<chat_id> message=4072
# (no further lines for chat=<chat_id> during the 12:13–12:31 EDT window)

So the agent emitted 7 valid status chunks, the runtime persisted all of them to the session JSONL, the work succeeded, and 0 messages were delivered to Telegram. Every chunk's phase value was commentary. Under final_only semantics that is correct rejection — but it is also the entire user-visible failure: the user-side outcome is identical to a hung session.

The agent's own NO_REPLY self-tagging at the close of the turn (also recorded in the JSONL) confirms it intended to ship something, then never re-entered a state where it produced a phase=final chunk.

What it is NOT

Expected behavior

Either:

(a) On turn-end (last assistant chunk before tool-call queue drains and the runtime emits the equivalent of agent_end), the runtime should automatically promote the last commentary chunk to phase=final if no explicit final chunk was produced. This preserves final_only semantics without dropping the entire reply on long tool-loop turns. Or

(b) The agent runtime should refuse to close a turn that has zero phase=final chunks while the user-facing channel is configured final_only — emit an explicit "ran out of tokens / context window / time without a final" diagnostic that the gateway can surface, rather than silently shipping nothing.

Today the runtime appears to do neither: it accepts a turn with 0 phase=final chunks and ships nothing under final_only, with no operator-visible signal that delivery was suppressed.

Suggested fix direction

  1. Auto-promote-last-commentary on turn-end — easiest path. When the runtime detects an agent turn closing with zero phase=final emissions and the channel's deliveryMode = "final_only", automatically re-tag the last sufficiently-substantial commentary chunk as phase=final. Preserves observability, preserves the fix from 2026-03-18, doesn't drop messages.
  2. Hard diagnostic on no-final-emission close — at minimum, emit a gateway-log warning like [stream] turn closed with 0 final-phase chunks; deliveryMode=final_only suppressed N commentary chunks. Right now the failure is utterly silent in the gateway log (we had to grep textSignature.phase in the session JSONL to find it).
  3. Per-channel deliveryMode fallback option — e.g. final_or_last_commentary. Lets operators opt into "deliver the final OR, if there isn't one, the last commentary chunk" without having to choose between the 2026-03-18 preview-finalized observability bug and the 2026-05-03 silent-suppression bug.

Related issues (cross-link these)

  • #76424 — Telegram context-overflow retry replays same inbound message and delivers stale turn. Same family (long tool-loops + Telegram + final_only); different mechanism (input-side replay vs. output-side never-final). Both contribute to "agent went silent on long-running turns" symptom.
  • #76772session_status leaks stale model identity via current alias and cached last-run model. Different surface entirely.
  • #74257 — Telegram async exec followup leaks HEARTBEAT_OK/internal text after context overflow. Adjacent: also about turn-end phase mishandling, but different direction (over-emits internal text rather than under-emits final text).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions