Skip to content

feat(slack): continue replies in owned threads - #446

Open
palrohitg wants to merge 6 commits into
OpenHands:mainfrom
palrohitg:feat/442-slack-thread-replies
Open

palrohitg wants to merge 6 commits into
OpenHands:mainfrom
palrohitg:feat/442-slack-thread-replies

Conversation

@palrohitg

@palrohitg palrohitg commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Why

Slack Socket Mode forwarded app_mention events, but an ordinary human reply in an existing Slack thread arrives as a message event. That prevented a user from naturally continuing an automation conversation without mentioning the bot again.

The issue's initial parent_user_id == bot_user_id proposal does not cover the normal conversational shape because Slack threads are flat: parent_user_id identifies the thread-root author. A human mention therefore creates a human-authored root even after the bot replies inside it.

What changed

  • Accept human-authored Slack message events when thread_ts is present.
  • Continue human-rooted replies only when the automation already owns the resolved thread subject.
  • Represent that boundary with AcceptedEvent.existing_subject_only, defaulting to false for existing transports.
  • Stop before run creation when a follow-up-only event has no matching owner, including dispatch_run triggers.
  • Preserve the explicit bot-rooted case from Respond to replies to the bot's comments, not only @-mentions #442: it may continue an existing subject or create a run.
  • Continue dropping bare channel messages, bot-authored messages, and unrelated human-rooted threads.
  • Leave existing app_mention-only automations unchanged.

Routing decision

Slack event Result
app_mention Existing behavior: continue the subject or create a run
Threaded message, bot-authored root Continue or create a run
Threaded message, human-authored root Continue only if this automation already owns the thread subject
Threaded message in an unrelated human thread Deduplicated, but no run is created
Bare channel message or bot-authored message Dropped by the Slack provider

The ownership check uses the existing durable AutomationRun.subject_key. It works across restarts and multiple Socket Mode replicas without a process-local thread cache, Slack history lookup, or schema migration.

An automation that wants natural follow-ups must listen for both event keys and resolve the opener and replies to the same subject:

{
  "type": "event",
  "source": "slack",
  "on": ["app_mention", "message"],
  "destination": "continue_conversation",
  "subject_key_expr": "join('/', [team_id, event.channel, event.thread_ts || event.ts])"
}

Verification

Regression coverage includes:

  • acknowledgement before routing for human-rooted and bot-rooted replies;
  • provider classification of human-rooted replies as follow-up-only;
  • mention → owned human-rooted reply continuing the same conversation;
  • an unrelated human-rooted thread creating no run;
  • a dispatch_run trigger being unable to bypass the follow-up-only guard;
  • the new flag defaulting off for existing callers;
  • bare messages and bot-authored messages remaining ignored.

Results:

  • uv run pre-commit run --files ... --show-diff-on-failure — passed (Ruff format/lint, pycodestyle, Pyright).
  • Non-Docker Slack provider suite — 25 passed, 5 deselected.
  • In-memory SQLite routing flow — passed.
  • Real Slack Socket Mode proof — human root mention → bot thread reply → human follow-up without another mention; the follow-up reused the owned conversation and finished with total runs: 1.
  • PostgreSQL-backed tests could not start locally because this environment cannot access its Docker socket; the test bodies were not entered.
  • Repository CI workflows are currently awaiting maintainer approval for this fork PR.

The live proof exercises the real Slack connection, SlackStreamProvider, accept_event(), durable subject lookup, and queued-run coalescing. It intentionally does not run an LLM or Agent Server; it verifies the routing responsibility owned by this repository.

HUMAN: I ran the real Slack mention and unmentioned follow-up flow and reviewed the token-free PASS output.

Operational scope

The Slack app must subscribe to message.channels and have the channels:history bot scope for public-channel replies.

The issue also mentions an event-driven slack-channel-monitor change in OpenHands/extensions. Current main there contains a cron-polling implementation and does not contain the referenced event_payload / app_mention guard, so this PR does not modify that separate implementation.

All temporary .pr/ review artifacts were removed in 094665a and will not be merged.

Fixes #442

@palrohitg

Copy link
Copy Markdown
Contributor Author

@VascoSch92 @enyst, could one of you take a look when you have a moment? This is a focused follow-up to the Socket Mode work from #384: existing app_mention behavior stays unchanged, and message events are accepted only for replies to the bot’s thread. Fork CI is currently waiting for approval.

@all-hands-bot

Copy link
Copy Markdown
Contributor

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

@palrohitg

Copy link
Copy Markdown
Contributor Author

The failed unit-test job is a shared CI dependency failure, not a Slack stream assertion failure: 1,703 tests passed, then all 17 S3 integration cases failed in fixture setup because Docker Hub no longer serves the pinned MinIO image. The registry repair is isolated in #447.

@VascoSch92

Copy link
Copy Markdown
Member

The implementation matches the spec in #442 exactly, but I think the spec itself is wrong.

Slack threads are flat, and parent_user_id is the thread root's author on every reply, not the author of the message being replied to. The conversations.replies example (https://docs.slack.dev/reference/methods/conversations.replies/) shows every reply carrying the root author's id.

So parent_user_id == bot_user_id means "the bot wrote the thread root", not "the user is replying to the bot". In the usual pattern, a human mentions the bot at top level and the automation answers in that thread, so the root stays the human's message and every follow-up is dropped. Running the shapes through accepted_event on this branch:

human top-level '@bot do X'  (app_mention)   -> ACCEPTED
human follow-up in that thread               -> DROPPED   <-- the case #442 wants
human reply under a bot-rooted top-level post -> ACCEPTED

The filter only fires when the bot posts a new top-level message, which is the digest case, not the conversational one. It should accept threads the bot has posted into, not only threads the bot rooted.

Could you please double check that? And also see if you can create an automation where you show the behaviour?

@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor
  ✅ **PR Artifacts Cleaned Up**

  The `.pr/` directory is no longer present.

@palrohitg palrohitg changed the title feat(slack): route replies to bot thread messages feat(slack): continue replies in owned threads Sep 13, 2026
@palrohitg

Copy link
Copy Markdown
Contributor Author

@VascoSch92 Thanks for catching the parent_user_id issue. I updated the routing to use durable subject ownership rather than assuming the bot authored the thread root.

I also ran the corrected flow against a real Slack workspace over Socket Mode:

  1. a human mentioned the bot at the thread root;
  2. the bot replied in that human-rooted thread;
  3. the human followed up without another @mention;
  4. the follow-up continued the same subject-owned conversation, with total runs: 1.

The harness exercised the real SlackStreamProvider, accept_event(), subject ownership lookup, and queued-run coalescing. It intentionally did not start an LLM or Agent Server; the terminal PASS is the routing assertion.

Slack thread

Human-rooted Slack thread with an unmentioned follow-up

Token-free terminal result

Live routing harness PASS with one run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Respond to replies to the bot's comments, not only @-mentions

3 participants