-
-
Notifications
You must be signed in to change notification settings - Fork 69.5k
[Feature]: Native Slack streaming via chat.startStream / appendStream / stopStream APIs #4391
Description
Summary
Add native real-time token streaming for Slack channels using Slack's chat.startStream, chat.appendStream, and chat.stopStream APIs. This replaces the current block-streaming approach (periodic message edits) with true streaming — tokens appear character-by-character as the LLM generates them, matching the experience users get in ChatGPT, Claude.ai, etc.
Motivation
Clawdbot's current Slack delivery has two modes:
- No streaming — entire response posted at once after generation completes
- Block streaming — response chunked and delivered via periodic message edits (
chat.update)
Block streaming is a significant improvement, but it's still noticeably "chunky" — text appears in bursts rather than flowing smoothly. Slack now offers native streaming APIs that deliver a much better UX.
Working Implementation
I've built and tested a complete implementation that's been running in production. Here's how it works:
Architecture
Two files:
dist/slack/chat-stream.js(NEW) — Streaming wrapper with throttling, error recovery, and graceful fallbackdist/slack/monitor/message-handler/dispatch.js(MODIFIED) — Wires streaming into the existing reply pipeline
Key API Details
Slack's streaming API has some poorly documented requirements discovered during implementation:
// Start a stream — creates a placeholder message that updates in real-time
const response = await client.chat.startStream({
channel,
thread_ts: threadTs, // REQUIRED — streaming only works in threads
recipient_team_id: teamId, // REQUIRED — poorly documented
recipient_user_id: userId, // REQUIRED — poorly documented
markdown_text: firstChunk,
});
const streamTs = response.ts; // message timestamp for subsequent calls
// Append more text (delta-based — send only new characters)
await client.chat.appendStream({
channel,
ts: streamTs,
markdown_text: deltaText,
});
// Finalize — locks the message with final content
await client.chat.stopStream({
channel,
ts: streamTs,
markdown_text: finalFullText, // optional: replace entire content
});Integration with dispatch.js
The stream hooks into the existing blockStreaming pipeline:
// In dispatch.js — stream creation
const canStreamNative = account.config.nativeStreaming === true ||
(account.config.nativeStreaming !== false && account.config.blockStreaming === true);
const chatStream = canStreamNative
? createSlackChatStream({
client: ctx.app.client,
channel: message.channel,
threadTs: initialThreadTs,
teamId: ctx.teamId || message.team,
userId: message.user,
token: ctx.botToken,
})
: undefined;
// Partial reply handler — feeds LLM tokens to the stream
onPartialReply: chatStream
? (payload) => { if (payload.text) chatStream.update(payload.text); }
: undefined,
// Delivery handler — stream IS the delivery, skip chat.postMessage
deliver: async (payload, info) => {
if (chatStream?.isStarted() && !chatStream.hasFailed()) {
const response = await chatStream.stop(payload.text);
if (response) {
replyPlan.markSent();
if (!hasMedia) return; // text delivered via stream
}
}
// Fallback to normal delivery
await deliverReplies({ ... });
}Design Decisions
- Thread-only: Slack streaming only works as threaded replies (
thread_tsrequired). Falls back to normal delivery for top-level messages. - Throttled: 300ms minimum between API calls to avoid rate limits. Uses a timer-based throttle similar to
telegram/draft-stream.js. - Delta-based: Only sends new characters (not full text each time), matching Slack's append model.
- Graceful fallback: If streaming fails at any point,
hasFailed()returns true and the delivery path falls through to normalchat.postMessage. - No duplicate messages: When stream succeeds, the deliver callback returns early — the streamed message IS the final message, no separate
chat.postMessageneeded. - Block streaming disabled during stream:
disableBlockStreaming: trueprevents the block chunking pipeline from also editing the message.
Config
Activates automatically when blockStreaming: true (existing config). Could also support a dedicated nativeStreaming: true flag.
Requirements
- Slack bot token with appropriate scopes
replyToMode: "all"or messages in existing threads (streaming requiresthread_ts)blockStreaming: truein channel config (or a newnativeStreamingflag)
Notes
recipient_team_idandrecipient_user_idare required by the Slack API but not well documented — took trial and error to discover- The stream wrapper pattern mirrors
telegram/draft-stream.jsfor consistency - This has been running in production on a real workspace with no issues
- Happy to contribute a PR if maintainers are interested