-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Summary
When a streaming response begins, there is a visible delay (sometimes up to ~1 minute) where a blank/empty message appears in Slack before any text renders. The first content push is blocked by a combination of two accumulation layers: the pendingStreamText redundancy-detection gate in reply-executor.ts and the createNormalizingStream newline-flush gate — causing the streaming post to be created but then stall with no visible content.
Root cause
Two buffering layers compound each other before any text reaches Slack's streaming API:
Layer 1 — pendingStreamText / redundancy gate (reply-executor.ts)
pendingStreamText += deltaText;
if (isPotentialRedundantReactionAckText(pendingStreamText)) {
return; // hold until text is "safe"
}
flushPendingStreamText(); // only then call startStreamingReply()isPotentialRedundantReactionAckText holds all incoming deltas until the accumulated text is clearly not a short reaction-only response (e.g. "ok", "got it", a partial emoji :thinking). For responses that begin with a brief acknowledgment phrase (e.g. "on it", "sure", "let me check"), every delta passes through this gate until the text grows long enough to no longer match any prefix of the redundant-ack list. This can be several tokens.
Layer 2 — createNormalizingStream newline gate (streaming.ts)
Once text is flushed to the textStream bridge, it is wrapped in createNormalizingStream(textStream.iterable, ensureBlockSpacing) before being passed to thread.post(). That stream only yields normalized output once a \n is seen (it accumulates everything into stable = accumulated.slice(0, lastNewline + 1) before normalizing). If the first batch of streamed text has no newline (very common for inline sentence-first responses), zero bytes are pushed to the Slack streamer.append() call.
Result: thread.post() is called with an async iterable (the normalizing stream) that yields nothing for an arbitrary period. In @chat-adapter/slack's stream() implementation, streamer.append() is called only once a delta is non-empty. So Slack sees no append calls while the message post has already been created — rendering a blank bubble in the UI.
The blank message is the Slack assistant thread shell created by chatStream() at the beginning of the stream call, before any content arrives.
Reproduction
- Send any message to junior that produces a response beginning with inline sentence text (no leading newline), especially if the first few tokens could match a potential redundant-ack prefix (e.g. response starts with "sure", "let me", "on it").
- Observe a blank Slack message bubble for 15–60+ seconds before text appears.
- Longer responses with tool calls are worse because the LLM may not emit any
onTextDeltafor a long time (tool execution), and then when it does the normalizing stream holds the first chunk until a newline arrives.
Expected behavior
- The streaming Slack message should show visible content within the first few tokens of text.
- At minimum, the
createNormalizingStreamshould eagerly yield content when no newline has been seen yet (the no-newline branch already doesyield delta— but this path is only hit afterpendingStreamTextis flushed, which itself waits for the redundancy gate). - Alternatively, the normalizing stream should not be applied to the initial post; normalization could be applied only to the content that reaches Slack's
editMessagefinalizer.
Potential fixes
-
Remove
createNormalizingStreamfrom the streaming path. TheensureBlockSpacingnormalization is only needed for the finaleditMessage/ non-streaming post. The Slackstream()adapter'sgetCommittableText()already handles incremental rendering correctly and doesn't need pre-normalization. -
Flush
pendingStreamTexteagerly after a timeout. If text has been accumulating for >N ms andstreamedReplyPromiseis still unset, force-flush regardless of the redundancy check result. -
Pass
createNormalizingStreamonly as the final text normalizer, not as the iterable fed tothread.post(). The streaming bridge (textStream) should carry raw deltas; normalization should happen at finalize time.
Files
packages/junior/src/chat/runtime/reply-executor.ts—pendingStreamTextgate,createNormalizingStreamwrappingpackages/junior/src/chat/runtime/streaming.ts—createNormalizingStream(newline-holdback behavior)vercel/chat(packages/adapter-slack/src/index.ts) —stream()impl,flushMarkdownDeltaonly fires on non-empty delta
Action taken on behalf of David Cramer.