Skip to content

[Bug]: deliverySucceeded=true returned when no adapter was invoked (early returns in deliverOutboundPayloads masquerade as success) #78532

@joeyfrasier

Description

@joeyfrasier

Filed as a follow-up to #77260 per @martingarramon's suggestion to split the
delivery telemetry bug from the visibleReplies regression. #77260 was closed
as the command-reply scope is fixed on main; this telemetry issue is still live.

Summary

deliverySucceeded flips to true whenever no per-payload onError fires,
including code paths that exit deliverOutboundPayloads without ever invoking
a channel adapter. Result: the gateway reports successful delivery for replies
that were never sent. There is no gateway.log line indicating suppression or
the silent no-op, so installs go dark for days without any signal.

Code references (current main)

  • src/agents/command/delivery.ts:357let deliverySucceeded = false; let deliveryHadError = false;
  • src/agents/command/delivery.ts:374deliveryHadError = true; (only set inside per-payload onError: markDeliveryError)
  • src/agents/command/delivery.ts:398deliverySucceeded = !deliveryHadError;
  • src/infra/outbound/deliver.ts:1084export async function deliverOutboundPayloads(...)
  • src/infra/outbound/deliver.ts:1140if (claimResult.status === "claimed-by-other-owner") { return []; } (early return, no onError invoked)

The !deliveryHadError semantics mean any return path that doesn't iterate
payloads — claim conflict, bindings-empty Discord threads, message_tool
plugin not in plugins.allow, etc. — returns to the caller with
deliveryHadError=false, and line 398 reports success.

Reproduced silent-success paths

  1. Claim conflictclaimed-by-other-owner at deliver.ts:1140 returns [] directly. Caller flips deliverySucceeded=true.
  2. Discord thread bindings empty — when ~/.openclaw/discord/thread-bindings.json is missing or its entry was evicted by a session-store cap fire, resolveBoundThreadBinding(sessionKey) returns empty, the Discord adapter is never invoked, and the call returns without any onError firing. CLI --reply-to probe with --deliver returned deliverySucceeded: true while Discord API confirmed the bot's reply never posted (last 15 messages 100% inbound over 7 days).
  3. message_tool plugin not in plugins.allow — same shape: adapter not invoked, no error, success reported.

In each case the trajectory shows session.started → ... → model.completed → trace.artifacts → session.ended with no delivery event between model.completed and session.ended, the session JSONL records the reply text, and deliverySucceeded returns true.

Why this matters

This is the difference between "agent looks broken for an hour" and "agent looks broken for 7 days." With deliverySucceeded=true lying, every operator-facing health check, log dashboard, and probe says delivery is fine, so nobody investigates. The actual missing message is only discoverable by querying the channel API directly (GET /channels/<id>/messages) and noticing the bot's reply isn't there.

This is structurally distinct from the message_tool_only regression in #77260:

Suggested fix shape

Default deliverySucceeded to false and only flip it true when an adapter explicitly succeeds (positive acknowledgment, not absence of error). Concretely:

  • Track deliveryAdapterSucceeded: boolean set by adapters on confirmed send.
  • Replace deliverySucceeded = !deliveryHadError with deliverySucceeded = deliveryAdapterSucceeded.
  • Audit every early return in deliverOutboundPayloads and downstream paths to confirm none of them reach the success flip without invoking an adapter.
  • Add a gateway.log line at WARN whenever delivery exits without adapter invocation, including the reason (claim-conflict, bindings-empty, tool-unavailable, etc.). Silent no-ops are the diagnostic killer here.

Suggested test additions

  • src/agents/command/delivery.test.ts — assert deliverySucceeded === false on the claimed-by-other-owner path.
  • src/infra/outbound/deliver.test.ts — assert deliverySucceeded === false when deliverOutboundPayloads exits without iterating payloads.
  • A Discord-thread variant asserting deliverySucceeded === false when resolveBoundThreadBinding returns empty.
  • A message_tool-not-in-plugins.allow variant asserting the same.

Environment of the original repro (from #77260)

  • OpenClaw 2026.5.4 (built from main, channel=dev)
  • macOS 14.x, npm install
  • Discord public thread (type=11) under guild text channel parent
  • messages.groupChat.visibleReplies = "message_tool" (default after doctor --fix)
  • message_tool plugin NOT in plugins.allow

Trajectory JSONL, session JSONL, and --reply-to probe transcript available
on request for regression-test seed data.

cc @martingarramon @vincentkoc — flagging per Martin's split suggestion in
#77260 (comment)

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