Skip to content

fix(slack): seed thread routing for implicit-conversation channels (#78505)#78522

Merged
bek91 merged 1 commit intoopenclaw:mainfrom
zeroth-blip:fix/slack-implicit-thread-routing
May 8, 2026
Merged

fix(slack): seed thread routing for implicit-conversation channels (#78505)#78522
bek91 merged 1 commit intoopenclaw:mainfrom
zeroth-blip:fix/slack-implicit-thread-routing

Conversation

@zeroth-blip
Copy link
Copy Markdown
Contributor

@zeroth-blip zeroth-blip commented May 6, 2026

Summary

  • Problem: In Slack channels with requireMention: false, the inbound root turn landed on the channel session (agent:main:slack:channel:<id>) while later replies inside the bot-created thread landed on a fresh :thread:<root_ts> session, splitting one logical conversation across two OpenClaw sessions.
  • Why it matters: The bot loses continuity with the message it just replied to as soon as the user enters the thread; session list also bloats with one extra session per Slack thread. Reported in Slack: outbound thread reply does not promote the session key to the thread, so the next inbound thread message lands in a fresh session #78505.
  • What changed: Extend seedTopLevelRoomThreadBySource in prepareSlackMessage to also fire when the channel will implicitly produce a Slack thread anyway (isRoom + requireMention: false + replyToMode != "off"). The root and its thread replies now share the same :thread:<root_ts> session key, the same way app_mention / explicit-mention roots already do.
  • What did NOT change (scope boundary): DMs, group DMs (MPIM), requireMention: true channels, replyToMode: "off" channels, app_mention / explicit-mention paths, regex mention paths, runtime conversation bindings. No new config field, no migration. Outbound routing precedence (replyToId vs threadId) is also intentionally untouched and tracked separately.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Real behavior proof (required for external PRs)

  • Behavior or issue addressed: Slack thread replies in requireMention: false channels split off into a separate OpenClaw session.

  • Real environment tested: OpenClaw 2026.5.5 (production install on macOS 26.3.1 / arm64), Slack Socket Mode, channel C0AGG76CP1S with requireMention: false, channels.slack.replyToMode = "first", messages.groupChat.visibleReplies = "automatic". Local hotfix (functionally identical to this patch) applied directly to the bundled dist/prepare-CD7Ym2Zf.js and the gateway restarted via openclaw gateway restart.

  • Exact steps or command run after this patch:

    1. Send a top-level message in #genai (no @mention).
    2. The bot replies inside the thread Slack creates under that message.
    3. Reply inside that same thread.
    4. Verify with openclaw status and the sessions list snapshot.
  • Evidence after fix (live terminal output, copied verbatim from the running gateway):

Gateway health after applying the source change and restarting:

$ openclaw status
…
│ Gateway              │ local · ws://127.0.0.1:18789 (local loopback) · reachable 33ms · auth token · …  │
│ Gateway service      │ LaunchAgent installed · loaded · running (pid 5029, state active)                │
│ Sessions             │ 7 active · default gpt-5.5 (200k ctx) · 2 stores                                  │
…

Live Slack sessions for channel C0AGG76CP1S (#genai) after the fix, captured via the OpenClaw runtime sessions API and copied verbatim:

agent:main:slack:channel:c0agg76cp1s:thread:1778073105.769279     ← original turn (root + thread reply share this session)
agent:main:slack:channel:c0agg76cp1s:thread:1778079873.776589     ← brand new top-level turn (also seeded as a thread session from the start)

Notably, no bare agent:main:slack:channel:c0agg76cp1s session is created for new top-level turns anymore — both root and thread reply land on the same :thread:<root_ts> key.

Redacted runtime log excerpt from the same machine after the patch (gateway pid 5029, Slack Socket Mode connected):

[gateway] restart applied (SIGUSR1) — pid=5029 mode=emit
[slack] account=default  channel=C0AGG76CP1S  inbound  source=message  isRoom=true  requireMention=false  replyToMode=first
[slack] seeding top-level room thread (implicit-conversation channel) → routedThreadId=<root_ts>
[slack] outbound delivery → channel=C0AGG76CP1S  thread_ts=<root_ts>  ok
[slack] inbound thread reply  thread_ts=<root_ts>  → session=agent:main:slack:channel:c0agg76cp1s:thread:<root_ts>  (same session as root)
  • Observed result after fix: Each top-level message in the #genai channel routes to a :thread:<root_ts>-scoped OpenClaw session from the very first turn. Follow-up replies inside the same Slack thread resolve to the same session key as the root (no more channel-vs-thread split). Verified live via openclaw status (gateway healthy, pid 5029) and the sessions list snapshot (terminal output above), captured immediately after restarting the gateway with the patched build. No new bare agent:main:slack:channel:c0agg76cp1s session was created for any new top-level turn during the verification window.
  • What was not tested: replyToMode = "batched" channels (logic identical, just no live channel handy); ACP-bound conversations (covered by existing tests).
  • Before evidence (optional): Same install, same channel, same scenario before the patch produced two distinct OpenClaw sessions per logical conversation: a bare agent:main:slack:channel:c0agg76cp1s session for the root + a fresh agent:main:slack:channel:c0agg76cp1s:thread:<root_ts> session for every later in-thread message (live observation captured in Slack: outbound thread reply does not promote the session key to the thread, so the next inbound thread message lands in a fresh session #78505).

Root Cause (if applicable)

  • Root cause: prepareSlackMessage computed seedTopLevelRoomThreadBySource from app_mention | wasMentioned | explicitlyMentioned. Channels configured with requireMention: false accept implicit prompts (no @mention required), but the seeding rule did not include that case. The root turn therefore did not seed a thread session, while the user's later in-thread replies — being actual thread_ts replies — did get a thread-scoped session, producing the asymmetry reported in Slack: outbound thread reply does not promote the session key to the thread, so the next inbound thread message lands in a fresh session #78505.
  • Missing detection / guardrail: No regression test exercised the requireMention: false + non-off replyToMode path through prepareSlackMessage end-to-end. Existing thread-session-key tests at the routing level only fed seedTopLevelRoomThread directly, bypassing the gate where the regression actually lives.
  • Contributing context: Fix Slack inbound thread session routing #72498 introduced the seeding mechanism for actionable mentions but didn't generalize it to implicit-conversation channels, presumably because that path is less common in mention-gated workspaces.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/slack/src/monitor/message-handler/prepare.test.ts
  • Scenario the test should lock in: A top-level requireMention: false channel turn with replyToMode: "first" and no @mention must produce a :thread:<root_ts> session key. A subsequent thread_ts-bearing reply must resolve to the same session key.
  • Why this is the smallest reliable guardrail: The bug lives in the prepareSlackMessage seeding gate, so only an end-to-end prepareSlackMessage assertion (not a pure routing-layer one) actually pins it. Reverting the source change locally makes the new test fail with the exact mismatch from Slack: outbound thread reply does not promote the session key to the thread, so the next inbound thread message lands in a fresh session #78505 (agent:main:slack:channel:c0agg76cp1s vs …:thread:1778073105.769279), confirming the test catches the regression.
  • Existing test that already covers this: None. The closest, "keeps a root app mention and URL-only Slack thread follow-up on one parent session", only covers the app_mention path.
  • If no new test is added, why not: N/A — a new regression test is added.

User-visible / Behavior Changes

For Slack channels with requireMention: false and a non-off replyToMode, top-level inbound messages now route to a thread-scoped session (agent:<agent>:slack:channel:<id>:thread:<root_ts>) from the first turn, instead of the bare channel session. This matches what those channels already do for app_mention / explicit-mention roots, and matches the visible Slack behavior (every top-level bot reply already lives inside a Slack thread on those channels).

Channels with requireMention: true, channels with replyToMode: "off", DMs, and group DMs are unchanged.

Diagram (if applicable)

Before (requireMention: false, replyToMode: "first"):
  User: top-level message in #channel
    -> session: agent:main:slack:channel:<id>
  Bot: replies inside Slack thread under user message
  User: replies in that thread
    -> session: agent:main:slack:channel:<id>:thread:<root_ts>   ← split

After:
  User: top-level message in #channel
    -> session: agent:main:slack:channel:<id>:thread:<root_ts>   ← seeded
  Bot: replies inside that thread
  User: replies in that thread
    -> session: agent:main:slack:channel:<id>:thread:<root_ts>   ← same session

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS 26.3.1 (arm64)
  • Runtime/container: OpenClaw 2026.5.5, Node 25.8.2
  • Model/provider: gpt-5.5 (irrelevant to the routing layer)
  • Integration/channel: Slack Socket Mode
  • Relevant config (redacted):
    • messages.groupChat.visibleReplies: "automatic"
    • channels.slack.replyToMode: "first"
    • channels.slack.replyToModeByChatType: { direct: "off", group: "first", channel: "first" }
    • channels.slack.thread: { historyScope: "thread", inheritParent: false }
    • channels.slack.channels.<channelId>: { enabled: true, requireMention: false }

Steps

  1. Send a top-level message in the configured channel (no @bot).
  2. Bot replies inside the auto-created Slack thread.
  3. Reply inside that thread.
  4. Inspect openclaw status / sessions list for the channel.

Expected

  • One OpenClaw session per Slack thread, thread-scoped from the root.
  • Follow-up replies inside the thread share that session.

Actual (with fix)

  • Matches expected: both turns route to agent:main:slack:channel:<id>:thread:<root_ts>.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets (see Real behavior proof above)
$ pnpm test extensions/slack/src/monitor/message-handler/prepare.test.ts \
              extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts
… 65 tests passed (55 + 10) — including the new regression test.

$ pnpm test extensions/slack
… 925 tests passed (91 files).

Regression confirmation:
  - With source change reverted, the new test fails with:
      AssertionError: expected 'agent:main:slack:channel:c0agg76cp1s'
      to be 'agent:main:slack:channel:c0agg76cp1s:thread:1778073105.769279'
  - With source change in place, the test passes.

Human Verification (required)

  • Verified scenarios (live Slack):
    • Top-level message + same-thread reply in a requireMention: false channel — both turns now share one :thread:<root_ts> session.
    • Existing app_mention and explicit-mention routes — still consolidate as before (existing tests).
    • DM and MPIM routing — unchanged (existing tests).
  • Edge cases checked:
    • replyToMode: "off" channels — gate stays disabled (existing test keeps top-level channel turns in one session when replyToMode=off still passes).
    • requireMention: true channels — gate stays disabled.
  • What I did not verify in a live environment:
    • replyToMode: "batched" (logic-equivalent, covered by unit tests).
    • Multi-account Slack setups (no second workspace available locally).

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Existing channels relying on the bare agent:<id>:slack:channel:<id> session for cross-thread continuity in a requireMention: false channel will now route per-thread.

@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. size: S labels May 6, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 6, 2026

Codex review: needs changes before merge.

Summary
The PR updates Slack message preparation to seed thread-scoped sessions for top-level requireMention: false room messages with non-off reply threading and adds prepare-level regression coverage.

Reproducibility: yes. at source level. Current main leaves implicit requireMention: false top-level room turns unseeded while later inbound thread replies route by thread_ts, and the PR body supplies live Slack before/after session-key evidence.

Real behavior proof
Sufficient (terminal): The PR body includes after-fix terminal output and redacted runtime logs from a real Slack Socket Mode setup showing the improved session routing.

Next step before merge
Queue a narrow repair because the only definite blocker is the missing active-release changelog line; leave the Slack routing diff intact.

Security
Cleared: The diff only changes Slack routing logic and tests, with no dependency, workflow, secret, permission, or new network-execution surface.

Review findings

  • [P3] Add the required changelog entry — extensions/slack/src/monitor/message-handler/prepare.ts:304
Review details

Best possible solution:

Add the active-release changelog entry, then merge the focused Slack routing fix after exact-head validation passes.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level. Current main leaves implicit requireMention: false top-level room turns unseeded while later inbound thread replies route by thread_ts, and the PR body supplies live Slack before/after session-key evidence.

Is this the best way to solve the issue?

Yes, with one process fix. Extending the existing prepare-level seed gate is the narrowest maintainable fix for this Slack session split, but the PR still needs the required changelog entry.

Full review comments:

  • [P3] Add the required changelog entry — extensions/slack/src/monitor/message-handler/prepare.ts:304
    This changes user-visible Slack session routing for requireMention: false channels. OpenClaw policy requires user-facing fix changes to add a single-line CHANGELOG.md entry under the active release before merge.
    Confidence: 0.92

Overall correctness: patch is correct
Overall confidence: 0.88

Acceptance criteria:

  • git diff --check
  • pnpm test extensions/slack/src/monitor/message-handler/prepare.test.ts extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts

What I checked:

Likely related people:

  • @bek91: Merged Slack inbound thread-routing work introduced the seeded room-thread route and tests that this PR extends. (role: introduced related behavior; confidence: high; commits: aac83e00cfe7; files: extensions/slack/src/monitor/message-handler/prepare.ts, extensions/slack/src/monitor/message-handler/prepare-routing.ts, extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts)
  • @vincentkoc: Recent Slack message hot-path maintenance touched the central prepare path adjacent to this routing gate. (role: recent maintainer; confidence: medium; commits: ac74a928456d, c0302512d4dd; files: extensions/slack/src/monitor/message-handler/prepare.ts)
  • @steipete: Recent Slack routing/session work touched prepare-routing.ts, including top-level DM and route-binding behavior near the same session-key boundary. (role: adjacent owner; confidence: medium; commits: d964488a23cd, 3e2a2c7b7436; files: extensions/slack/src/monitor/message-handler/prepare-routing.ts, extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts)

Remaining risk / open question:

  • Exact-head PR validation still needs to gate merge after the changelog repair.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 4721ca8e459f.

Re-review progress:

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 6, 2026
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 6, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 6, 2026
When a Slack channel has `requireMention: false` and a non-`off` reply mode, every top-level bot reply creates a Slack thread (because `replyToMode` does). Without seeding the inbound root, the root turn landed on the channel session while later thread replies landed on a fresh `:thread:<root_ts>` session, breaking conversational continuity.

Extend `seedTopLevelRoomThreadBySource` to also fire for those channels, mirroring how `app_mention` / `explicitlyMentioned` roots already get seeded. The thread session key is now consistent on both sides of the turn, so follow-up thread messages route back to the originating session.

Fixes openclaw#78505
@zeroth-blip zeroth-blip force-pushed the fix/slack-implicit-thread-routing branch from 179a0d8 to 7ed9ed5 Compare May 7, 2026 07:08
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 7, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 7, 2026
@bek91
Copy link
Copy Markdown
Contributor

bek91 commented May 8, 2026

@zeroth-blip thanks for catching this, reviewed and this is the right fix. Il push a separate docs + changelog fix as the docs need to catch up.

The current docs say channel sessions are agent: <agentId>:slack:channel:<channelId> and thread replies “can” create :thread:<threadTs> suffixes, but they do not clearly state that top-level roots can be seeded into thread sessions when OpenClaw will reply in-thread.

@bek91 bek91 merged commit 741315e into openclaw:main May 8, 2026
111 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…penclaw#78522)

When a Slack channel has `requireMention: false` and a non-`off` reply mode, every top-level bot reply creates a Slack thread (because `replyToMode` does). Without seeding the inbound root, the root turn landed on the channel session while later thread replies landed on a fresh `:thread:<root_ts>` session, breaking conversational continuity.

Extend `seedTopLevelRoomThreadBySource` to also fire for those channels, mirroring how `app_mention` / `explicitlyMentioned` roots already get seeded. The thread session key is now consistent on both sides of the turn, so follow-up thread messages route back to the originating session.

Fixes openclaw#78505
rogerdigital pushed a commit to rogerdigital/openclaw that referenced this pull request May 9, 2026
…penclaw#78522)

When a Slack channel has `requireMention: false` and a non-`off` reply mode, every top-level bot reply creates a Slack thread (because `replyToMode` does). Without seeding the inbound root, the root turn landed on the channel session while later thread replies landed on a fresh `:thread:<root_ts>` session, breaking conversational continuity.

Extend `seedTopLevelRoomThreadBySource` to also fire for those channels, mirroring how `app_mention` / `explicitlyMentioned` roots already get seeded. The thread session key is now consistent on both sides of the turn, so follow-up thread messages route back to the originating session.

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

Labels

channel: slack Channel integration: slack proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slack: outbound thread reply does not promote the session key to the thread, so the next inbound thread message lands in a fresh session

2 participants