-
-
Notifications
You must be signed in to change notification settings - Fork 68.8k
Outbound Telegram messages arrive out of order when multiple tool results fire concurrently #11044
Description
Bug Description
When the agent triggers multiple tool calls in a single turn, the resulting Telegram messages frequently appear out of order — the first message appears last and vice versa. This is a consistent, user-visible issue.
Root Cause
Tool result messages in src/auto-reply/reply/agent-runner-execution.ts are dispatched in a fire-and-forget pattern. Each tool result creates an independent async task that is added to pendingToolTasks without awaiting previous results:
onToolResult: onToolResult ? (payload) => {
const task = (async () => {
const { text, skip } = normalizeStreamingText(payload);
if (skip) return;
await params.typingSignals.signalTextDelta(text);
await onToolResult({ text, mediaUrls: payload.mediaUrls });
})().catch((err) => {
logVerbose(`tool result delivery failed: ${String(err)}`);
}).finally(() => {
params.pendingToolTasks.delete(task);
});
params.pendingToolTasks.add(task); // No await — runs concurrently
} : void 0pendingToolTasks is only awaited collectively via Promise.allSettled at the end of the turn — after messages have already been sent to Telegram. Network latency jitter causes later-dispatched messages to arrive before earlier ones.
Contributing factors
- No global send serialization across concurrent reply types — block replies use
sendChain, but tool results bypass it entirely. - apiThrottler (
@grammyjs/transformer-throttler) can batch-release queued requests during rate limit recovery, amplifying reordering. - The
sequentializemiddleware only handles inbound ordering, not outbound.
Reproduction
- Send a message that triggers 2+ independent tool calls (e.g., two parallel
readorweb_searchcalls) - Observe the tool result messages arriving in reversed or shuffled order in Telegram
Suggested Fix
Serialize tool result delivery using a promise chain instead of fire-and-forget:
let toolResultChain = Promise.resolve();
onToolResult: onToolResult ? (payload) => {
toolResultChain = toolResultChain.then(async () => {
const { text, skip } = normalizeStreamingText(payload);
if (skip) return;
await params.typingSignals.signalTextDelta(text);
await onToolResult({ text, mediaUrls: payload.mediaUrls });
}).catch((err) => {
logVerbose(`tool result delivery failed: ${String(err)}`);
});
} : void 0For a more robust fix, a per-chat global send queue would prevent interleaving across all reply types (block replies, tool results, final replies).
Environment
- OpenClaw latest (npm)
- Channel: Telegram
- macOS (Apple Silicon)
Severity
High — frequently visible to end users, degrades conversation readability.