openclaw - ✅(Solved) Fix [Bug]: Slack preview streaming race condition causes double messages for fast LLM responses [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#54857Fetched 2026-04-08 01:35:10
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
cross-referenced ×2referenced ×2

When preview streaming is enabled (the default), fast LLM responses complete before the stream throttle fires (default 1000ms). This causes deliver() to read draftMessageId while it is still undefined, fall through to deliverNormally(), and post a second independent message — resulting in two visible Slack messages (one from the stream draft, one from the normal delivery path).

Root Cause

In the deliver() callback inside the Slack reply dispatcher, draftMessageId is read immediately after hasStreamedMessage is set to true:

const draftMessageId = draftStream?.messageId(); // ← undefined if throttle hasn't fired yet

The condition gating the edit-in-place path requires typeof draftMessageId === "string". When the LLM finishes faster than the 1000ms throttle, the draft hasn't posted yet, so draftMessageId is undefined, the condition fails, and deliverNormally() posts a second message.

The draft stream then fires ~700ms later and posts message A. The final reply is posted as message B. Two messages, but only one "delivered reply" log entry — which is why the logs look clean.

Fix Action

Fix

Flush the pending draft synchronously before reading draftMessageId, so the message ID is guaranteed to be set if streaming was active:

// before:
const draftMessageId = draftStream?.messageId();

// after:
if (hasStreamedMessage && draftStream && !draftStream.messageId()) await draftStream.flush();
const draftMessageId = draftStream?.messageId();

This is a one-liner applied immediately before the draftMessageId read in the deliver function of the Slack reply dispatcher (file: runtime-api-*.js).

PR fix notes

PR #54924: fix: flush draft stream before reading messageId to prevent double Slack messages

Description (problem / solution / changelog)

Fixes #54857

Problem

Fast LLM responses complete before the Slack stream throttle (1000ms) fires. deliver() reads draftMessageId as undefined, falls through to deliverNormally(), and posts a duplicate message.

Fix

Flush the pending draft synchronously before reading draftMessageId:

if (hasStreamedMessage && draftStream && !draftStream.messageId()) await draftStream.flush();
const draftMessageId = draftStream?.messageId();

Impact

Eliminates double messages in Slack for fast/short responses. No change for longer responses where throttle already fires before delivery.

Changed files

  • extensions/slack/src/monitor/message-handler/dispatch.ts (modified, +3/-0)

PR #54937: fix(slack): flush draft before reading messageId to prevent double messages on fast responses

Description (problem / solution / changelog)

Summary

When an LLM responds faster than the 1000ms Slack draft-stream throttle, messageId() returns undefined at the time deliver() is called. This causes canFinalizeViaPreviewEdit to evaluate to false, so the code falls through to deliverNormally() and posts a second independent message. The draft-stream throttle then fires shortly after, posting the original draft — resulting in two visible Slack messages.

Root Cause

In extensions/slack/src/monitor/message-handler/dispatch.ts, draftMessageId was read before any pending draft update had been flushed:

// Before fix — messageId() is undefined if throttle hasn't fired yet
const draftMessageId = draftStream?.messageId();

Fix

Call draftStream.flush() before reading messageId(). flush() cancels the pending timer and synchronously sends the queued draft text, guaranteeing messageId() is populated when preview streaming is active.

// After fix
await draftStream?.flush();
const draftMessageId = draftStream?.messageId();

Testing

Added a regression test in extensions/slack/src/draft-stream.test.ts asserting that messageId() is undefined before flush() and non-undefined after — directly validating the guarantee the fix relies on.

Fixes openclaw/openclaw#54857


AI Assistance

  • AI-assisted (Claude Sonnet 4.6 via OpenClaw)
  • Lightly tested — pnpm check (lint, format, type checks, boundary checks) passed; targeted unit tests added/verified
  • Full pnpm build && pnpm test not run (CI will validate)
  • Code reviewed and understood by human author
  • Codex review not run locally; CI Codex review will apply

Extension test results

pnpm test:extension slack: 57 test files, 451 tests — all passed

Changed files

  • extensions/slack/src/draft-stream.test.ts (modified, +12/-0)
  • extensions/slack/src/monitor/message-handler/dispatch.ts (modified, +10/-0)

Code Example

const draftMessageId = draftStream?.messageId(); // ← undefined if throttle hasn't fired yet

---

// before:
const draftMessageId = draftStream?.messageId();

// after:
if (hasStreamedMessage && draftStream && !draftStream.messageId()) await draftStream.flush();
const draftMessageId = draftStream?.messageId();
RAW_BUFFERClick to expand / collapse

Description

When preview streaming is enabled (the default), fast LLM responses complete before the stream throttle fires (default 1000ms). This causes deliver() to read draftMessageId while it is still undefined, fall through to deliverNormally(), and post a second independent message — resulting in two visible Slack messages (one from the stream draft, one from the normal delivery path).

Steps to Reproduce

  1. Use Slack channel with preview streaming enabled (default config)
  2. Send a short, simple message to the agent (e.g. "are you there?")
  3. LLM responds quickly (under ~1 second)
  4. Observe two messages in Slack — one clean, one marked (edited)

Root Cause

In the deliver() callback inside the Slack reply dispatcher, draftMessageId is read immediately after hasStreamedMessage is set to true:

const draftMessageId = draftStream?.messageId(); // ← undefined if throttle hasn't fired yet

The condition gating the edit-in-place path requires typeof draftMessageId === "string". When the LLM finishes faster than the 1000ms throttle, the draft hasn't posted yet, so draftMessageId is undefined, the condition fails, and deliverNormally() posts a second message.

The draft stream then fires ~700ms later and posts message A. The final reply is posted as message B. Two messages, but only one "delivered reply" log entry — which is why the logs look clean.

Fix

Flush the pending draft synchronously before reading draftMessageId, so the message ID is guaranteed to be set if streaming was active:

// before:
const draftMessageId = draftStream?.messageId();

// after:
if (hasStreamedMessage && draftStream && !draftStream.messageId()) await draftStream.flush();
const draftMessageId = draftStream?.messageId();

This is a one-liner applied immediately before the draftMessageId read in the deliver function of the Slack reply dispatcher (file: runtime-api-*.js).

Impact

  • Affects all Slack deployments with preview streaming enabled (default)
  • Particularly visible for short/fast responses (greetings, confirmations, heartbeats)
  • Longer responses are unaffected because the throttle fires before deliver() is called

Environment

  • OpenClaw version: confirmed on 2026.3.23-2 and earlier; patched on 2026.3.26+
  • Channel: Slack (Socket Mode)
  • Streaming mode: default (preview / draft mode)
  • OS: macOS (arm64)

extent analysis

Fix Plan

To fix the issue of duplicate messages in Slack due to the draftMessageId being undefined when the LLM responds quickly, follow these steps:

  • Locate the deliver function in the Slack reply dispatcher file (runtime-api-*.js).
  • Add a synchronous flush of the pending draft before reading draftMessageId:
if (hasStreamedMessage && draftStream && !draftStream.messageId()) await draftStream.flush();
const draftMessageId = draftStream?.messageId();

This ensures that the draftMessageId is set if streaming was active.

Verification

To verify that the fix worked:

  1. Enable preview streaming in your Slack channel (default config).
  2. Send a short, simple message to the agent (e.g., "are you there?").
  3. Observe that only one message is posted in Slack.
  4. Check the logs for a single "delivered reply" log entry.

Extra Tips

  • This fix affects all Slack deployments with preview streaming enabled (default).
  • The issue is particularly visible for short/fast responses.
  • Make sure to update to OpenClaw version 2026.3.26+ or later to get the patched version.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING