Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-ducks-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chat-adapter/slack": minor
---

Rotate long-running native Slack streams before Slack expires them. Once a stream segment passes `streamSegmentMaxAgeMs` (default four minutes) the adapter finalizes it at the next paragraph break and continues the reply in a new message, closing and reopening code fences, repeating table headers, replaying open task cards and the plan title, and keeping the agent session in `processing`. A segment Slack already expired during an idle gap is recovered the same way instead of failing the reply.
8 changes: 8 additions & 0 deletions apps/docs/content/adapters/official/slack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ bot.onNewMention(async (thread, message) => {
description:
"Use Slack's native streaming API for streamed posts. Set false to always stream via post-and-edit.",
},
streamSegmentMaxAgeMs: {
type: "number",
default: "240000",
description:
"Finalize and continue native streams in a new message after this many milliseconds to stay below Slack's roughly five-minute stream expiry. Rotation waits up to 30 seconds for a paragraph break. Set Infinity to disable.",
},
feedbackButtons: {
type: "boolean | SlackFeedbackButtonsOptions",
description:
Expand Down Expand Up @@ -828,6 +834,8 @@ Threads without streaming context fall back to post-and-edit (`chat.update` delt
const slack = createSlackAdapter({ nativeStreaming: false });
```

Slack expires a native stream after roughly five minutes. A reply that streams longer than that is finalized and continued in a new message: once a segment is four minutes old (`streamSegmentMaxAgeMs`, default `240000`; `Infinity` disables rotation), the adapter rotates at the next paragraph break, or after at most 30 more seconds if none arrives. Across the boundary an open code fence is closed and reopened, a table that continues gets its header repeated, the plan title and any task cards still in progress are replayed so later updates land on them, and with `agentView` the session stays in `processing`. The finalized message keeps its task cards in their last state, and a list split across the boundary restarts its numbering. The `SentMessage` returned by `thread.post()` refers to the last message of the reply. If Slack expires a segment during a long idle gap anyway, the adapter continues in a new message with any text Slack had not confirmed rather than failing the reply.

#### Feedback buttons

Slack's agent UX guidance recommends native thumbs up/down feedback on agent replies (a `context_actions` block with a `feedback_buttons` element). Configure `feedbackButtons` and the adapter appends them to every streamed reply when the stream finishes:
Expand Down
14 changes: 13 additions & 1 deletion packages/adapter-slack/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,19 @@ Fallback to post-and-edit (`chat.update` deltas, throttled by
skipped and text streaming continues.

Failures after native content has rendered still propagate — mixing the
two surfaces would duplicate output.
two surfaces would duplicate output. The one exception is Slack expiring
a stream (`message_not_in_streaming_state`): the adapter continues in a
new message with any text Slack had not confirmed.

Slack expires a native stream after roughly five minutes, so `stream()`
works in segments: once a segment is `streamSegmentMaxAgeMs` old (the
clock starts at the first call Slack accepts, not at construction), the
next paragraph break (or, after a 30 s grace, the next line break)
finalizes it and a new `chatStream` continues the reply. Rotation closes
and reopens an open code fence, repeats a table header, replays the plan
and open task cards, and keeps `session_status: "processing"` with
`agentView`. Tests drive this with a mocked `Date.now()`; see the
"native stream rotation" describe block.

## Socket Mode

Expand Down
Loading