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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions