feat(api): queue Slack replies that arrive mid-turn and hand them back after
Merged
AV
2mo ago+454+454+012345678901234567890123456789-43-43-01234567890123456789
Links
- Plan page (Phase 1, PR B): https://app.superset.sh/page/slack-agent-plan-98cwoq
- Builds on #7583 (thread sessions). Sibling #7585 (plugin bridge) is independent.
Summary
- A reply that arrives while the thread's agent is mid-turn is queued instead of run concurrently, and handed back as its own turn when the running one finishes.
- The idle-to-running transition is now atomic, closing the concurrency finding deferred from #7583.
- Thread context flags messages newer than the agent's last reply as
[new].
Why / Context
With follow-ups reaching the agent without a mention, two replies in one thread could run at once and interleave. The queued_events column reserved in #7583 is the queue.
How It Works
beginThreadRunreturns{ status: "running", session }or{ status: "queued" }. It first triesUPDATE โฆ WHERE status = 'idle' OR last_activity_at < now() - 6 min(a stale run belongs to a dead worker), thenINSERT โฆ ON CONFLICT DO NOTHING, and otherwise appends the event toqueued_events, newest first, capped at 20.- A queued delivery releases its execution claim (
releaseAgentDelivery), keeps its ๐, clears any placeholder, and returns. Nothing is posted. - In the handler's
finally, afterfinishThreadRunmarks the session idle,takeQueuedEventsempties the queue atomically. If anything was waiting, the newest event is re-published to the mention job withdeduplicationId: queued:<team>:<ts>and the older queued timestamps inqueued_ts. That job takes the thread normally; its thread context contains the older queued messages, and it removes their reactions along with its own. fetchThreadContexttakessinceTs(the session'slast_context_ts) and marks later messages[new]with a count in the header.
Manual QA Checklist
Flag slack-thread-follow-ups is on for the Superset workspace.
- Mention the bot with a slow ask, then reply twice in the thread while it's thinking: both replies get ๐, nothing else posts until the first reply lands
- After the first reply, one more turn runs for the second follow-up, its answer accounts for both, and all three ๐ clear
-
select status, queued_events from slack_thread_sessionsisidleand[]afterwards - A thread whose worker died (status
running,last_activity_atolder than 6 minutes) accepts a new reply
Testing
bun test src/app/api/integrations/slackinapps/api: 75 pass (5 new: queued path, hand-back publish, reaction clearing for queued messages, last-context pass-through,[new]marking)bunx tsc --noEmit -p apps/api/tsconfig.json: cleanbunx biome check: clean- No schema change. Not exercised against live Slack.
Known Limitations
- Only the newest queued message is re-delivered as a prompt; older ones are context. That matches how a person would read a thread they'd stepped away from, but a queued message that was itself a distinct request gets answered as part of the newest one.
- The re-delivery goes through QStash, so a queued reply's turn starts a few seconds after the previous one ends.
https://claude.ai/code/session_01YDDSQYG6WSZn4jXWTR3Pid
Summary by cubic
Under the slack-thread-follow-ups flag, Slack replies that arrive while the agent is mid-turn are queued and handed back after the running turn finishes, so replies in one thread no longer run concurrently and interleave.
Behavior
- The idle-to-running claim is atomic; a run older than 6 minutes is treated as a dead worker and can be claimed.
- Only a running turn can accept queued replies; if the turn finishes first, the reply is claimed as its own turn.
- A reply that arrives mid-turn keeps its ๐ and posts nothing; its execution claim is released and the finalizer skips finishing work.
- When the turn finishes, the newest queued message is re-delivered via QStash as its own turn; older queued messages arrive as thread context, and that turn clears all their ๐.
- Each hand-back claims a delivery of its own and uses a per-hand-off QStash deduplication ID, so a handed-back reply that waits behind another turn can be handed back again without being dropped or swallowed.
- Queued replies carry their attachments through the queue to the hand-back.
- Queued events are kept newest-first and capped at 20.
- The queue is cleared only after QStash accepts the re-delivery; a failed publish leaves the replies for the next turn.
- A DM's queued replies go back through the assistant job, not the mention job.
- Thread context marks messages newer than the agent's last reply as
[new], with a count in the header. - With the flag off, no session is opened and nothing is queued or re-delivered.
Summary by CodeRabbit
-
New Features
- Slack agent conversations now queue messages received while the agent is responding.
- Queued messages are automatically re-delivered after the current response, with duplicate handling to prevent repeated processing.
- Queued messages retain their file attachments during re-delivery.
- The agent now distinguishes new thread messages from previously reviewed context.
- Agents can view and update whether a Slack thread is quiet or open.
-
Bug Fixes
- Improved handling of overlapping Slack messages so replies are less likely to be missed or processed out of order.
- Queued messages remain recoverable if re-delivery fails.
- Progress indicators are cleared appropriately when messages are queued.