Continue existing Duo session on repeat Slack mentions

What does this MR do and why?

Makes a Slack thread keep talking to the same Duo session, so mentioning the bot again continues the conversation instead of starting from nothing. The plumbing to run another turn against an existing session landed separately; this connects it to Slack and moves the flow onto the surface that makes it viable.

All of it sits behind the default-off slack_duo_api_flow flag. With the flag off, the CI-executed developer/v1 path is untouched.

Finding the session again

A new Ai::Messaging::SlackThreadSessionStore maps a Slack thread to the session answering it, in Redis, with a 7-day expiry refreshed on every turn.

Redis rather than a column on the workflow, because this is a routing hint and not a fact about the session. Losing it costs the user their context, not their data, and the next mention simply opens a new session. Storing it on the workflow would also mean indexing a jsonb column to find a session by Slack thread. Redis failures are tracked and swallowed for the same reason: answering without the earlier context beats not answering.

The key includes the GitLab user id and not only the thread. Slack sessions are private to whoever started them, and the endpoint a turn runs against accepts only the session's owner, so two people addressing the bot in one thread hold two independent conversations rather than fighting over one.

Deciding whether to continue

AppMentionedService looks the thread up and continues a session only when it belongs to the mentioning user, was created for the same flow, and is in a status the flow can resume from.

Sessions that are still running count as resumable, which is worth stating plainly since it looks permissive. Whether an earlier turn is genuinely in flight is the workflow lock's decision and it makes it, refusing a concurrent turn with a message the user can act on. Excluding running sessions here would instead strand a thread forever on a session whose worker died mid-turn.

Sending only what is new

A follow-up carries the thread messages posted since the flow last answered, minus the bot's own posts, which the flow wrote and would otherwise read back as if a user had said them. The recent channel history is not re-sent either; the session was given it when it opened. Besides being cheaper in tokens, this drops a Slack API call per follow-up, which matters because conversation history calls are the ones Slack rate-limits hardest.

Moving the flow to the chat surface

The last commit changes the flow's catalog entry from environment: ambient to chat, matching what the flow's own config in the Duo Workflow Service already declares.

This is not cosmetic. It is what makes the whole feature usable, and it pairs with a Duo Workflow Service change that ends each turn on input_required so the session survives its answer. ambient is one of the environments from_pipeline? treats as pipeline-executed, and a pipeline-executed flow reaching input_required means a human is being waited on, so UpdateWorkflowStatusService raises a to-do and emails the user. For this flow that state is simply the end of an answer. Without this commit, every single Slack message would produce a to-do and an email.

chat is also the more accurate label on its own terms: nothing about these sessions is ambient. Someone is talking to the assistant and waiting for the reply, and Workhorse runs them rather than a pipeline.

One consequence for reviewers to weigh. Tool governance keys off the same attribute. ambient resolved to no surface, which callers read as the web surface, so namespace tool rules applied. chat is a local surface, so it resolves to :chat with duo_workflow_local_tool_governance on and :ungoverned with it off. That is how the other local surfaces already behave, and this flow's toolset is fixed in its own config, but it is a loosening while that flag is off and should be an explicit decision rather than a side effect.

Not included

No documentation change. The Slack documentation describes the CI-executed path, which is what users get while the flag is off, and describing flag-only behaviour beside it would mislead. Docs land with the rollout.

References

Deployment order

Two ordering constraints, both one-directional:

  1. This MR adds the second argument to ServerSideTurnWorker.perform_async. Per doc/development/sidekiq/compatibility_across_updates.md, it must not deploy in the same release as the MR that added the parameter.
  2. The environment: chat commit should reach production before the Duo Workflow Service change. Rails first is harmless; the reverse produces a to-do and an email per Slack message.

Screenshots or screen recordings

Not applicable: no UI change. The user-visible effect is in Slack, where a follow-up mention is answered with the earlier exchange in context.

How to set up and validate locally

  1. Turn the flag on for yourself:

    Feature.enable(:slack_duo_api_flow)
  2. Check out the paired Duo Workflow Service branch, feature/slack-assistant-human-input, so a turn ends on input_required rather than running to completion.

  3. Mention the bot in a Slack channel and wait for the answer.

  4. Mention it again in the same thread, referring back to the first answer without repeating it, for example "and who maintains it?".

  5. Confirm the follow-up is answered in context, and that both turns belong to one session:

    Ai::DuoWorkflows::Workflow.where(workflow_definition: 'slack_assistant/v1')
      .order(id: :desc).limit(3)
      .pluck(:id, :status, :goal)

    Before this change each mention produced a row. Now the thread has one row whose checkpoint count grows per turn, sitting at input_required between turns.

  6. Confirm no to-do or email was raised for either turn.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Igor Drozdov

Merge request reports

Loading
Loading