feat(api): queue Slack replies that arrive mid-turn and hand them back after

Merged
AV
2mo ago
+454-43

Links

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

  • beginThreadRun returns { status: "running", session } or { status: "queued" }. It first tries UPDATE โ€ฆ WHERE status = 'idle' OR last_activity_at < now() - 6 min (a stale run belongs to a dead worker), then INSERT โ€ฆ ON CONFLICT DO NOTHING, and otherwise appends the event to queued_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, after finishThreadRun marks the session idle, takeQueuedEvents empties the queue atomically. If anything was waiting, the newest event is re-published to the mention job with deduplicationId: queued:<team>:<ts> and the older queued timestamps in queued_ts. That job takes the thread normally; its thread context contains the older queued messages, and it removes their reactions along with its own.
  • fetchThreadContext takes sinceTs (the session's last_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_sessions is idle and [] afterwards
  • A thread whose worker died (status running, last_activity_at older than 6 minutes) accepts a new reply

Testing

  • bun test src/app/api/integrations/slack in apps/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: clean
  • bunx 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.