Skip to content

Channel outbound: race condition between auto-reply and message tool sends #11614

@joshjhall

Description

@joshjhall

Description

When an agent uses the message tool during a turn AND the turn produces an auto-reply, the sends race because they use independent code paths:

  • Auto-reply → core delivery pipeline → ChannelOutboundAdapter.sendText()
  • Message toolChannelMessageAction handler → direct channel send

Both converge on the same underlying channel send function, but with no ordering coordination. On channels with timestamp-based message ordering (Matrix, potentially others), this causes messages to appear out of order.

Steps to Reproduce

  1. Configure a Matrix channel
  2. Have the agent send a message via the message tool (e.g. sending media)
  3. Have the agent also produce a text auto-reply in the same turn
  4. Observe that the auto-reply and tool-sent message appear in unpredictable order

Expected Behavior

All outbound messages to the same room/channel should be serialized to preserve send order, regardless of whether they originate from the auto-reply path or the message tool path.

Proposed Fix

Introduce a per-room/channel send queue at the channel plugin level that both the outbound adapter and tool-action handler route through. Each send awaits the previous one, ensuring consistent ordering without artificial delays.

// Conceptual example
class ChannelSendQueue {
  private queues = new Map<string, Promise<void>>();
  
  async send(roomId: string, fn: () => Promise<Result>): Promise<Result> {
    const prev = this.queues.get(roomId) ?? Promise.resolve();
    const next = prev.then(() => fn());
    this.queues.set(roomId, next.then(() => {}));
    return next;
  }
}

This could live in the core outbound layer (benefiting all channels) or be implemented per-channel plugin.

Environment

  • OpenClaw 2026.2.6
  • Matrix channel plugin (@openclaw/matrix 2026.2.3)
  • Matrix server: Tuwunel (Conduit fork)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstaleMarked as stale due to inactivity

    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