Skip to content

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

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

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions