Skip to content

fix(slack): keep top-level channel turns in one session when replyToMode=off#32193

Merged
steipete merged 2 commits intoopenclaw:mainfrom
bmendonca3:bm/slack-top-level-session-key
Mar 2, 2026
Merged

fix(slack): keep top-level channel turns in one session when replyToMode=off#32193
steipete merged 2 commits intoopenclaw:mainfrom
bmendonca3:bm/slack-top-level-session-key

Conversation

@bmendonca3
Copy link

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: Slack top-level channel messages were always forced into :thread:<message.ts> session keys, so each new top-level turn created a fresh isolated session.
  • Why it matters: In replyToMode=off (default), users lose channel conversation continuity and the agent appears to "forget" context across normal channel chat.
  • What changed: Slack session-key derivation now keeps top-level room turns channel-scoped when replyToMode=off; true thread replies remain thread-scoped.
  • What did NOT change (scope boundary): No changes to outbound routing, no changes to non-Slack channels, and no change to thread-reply behavior.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • 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

User-visible / Behavior Changes

  • Slack replyToMode=off: top-level channel/group messages now stay in one channel session instead of splitting per message.
  • Slack thread replies still use parent thread_ts session keys.
  • Slack replyToMode=all remains thread-scoped for top-level room turns.

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22.22.0, pnpm workspace
  • Model/provider: N/A (session-key routing path)
  • Integration/channel (if any): Slack
  • Relevant config (redacted): channels.slack.replyToMode: "off"

Steps

  1. Send two top-level Slack channel messages from the same sender with different ts values and no thread_ts.
  2. Build inbound Slack context (prepareSlackMessage) with replyToMode=off.
  3. Compare resolved ctxPayload.SessionKey values.

Expected

  • Both top-level messages resolve to the same channel session key (no per-message :thread:<ts> suffix).

Actual

  • Before this patch, each top-level message resolved to a different :thread:<message.ts> session key.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • pnpm -s vitest run src/slack/monitor/message-handler/prepare.thread-session-key.test.ts
    • pnpm -s vitest run src/slack/monitor/message-handler/prepare.test.ts
    • pnpm -s vitest run src/slack/monitor.threading.missing-thread-ts.test.ts
    • pnpm -s vitest run src/slack/monitor/message-handler.test.ts
  • Edge cases checked:
    • Thread replies still pinned to parent thread_ts.
    • replyToMode=all still uses thread-scoped room turns.
    • DMs with replyToMode=off remain non-thread-scoped.
  • What you did not verify:
    • Live Slack integration run against a real workspace in this branch.
    • Full pnpm check currently fails on upstream main due unrelated pre-existing TS error in src/discord/monitor/agent-components.ts:873.

Compatibility / Migration

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

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert commit b0e42039d8c0b11d37d54d19aecf413398b4325d.
  • Files/config to restore:
    • src/slack/monitor/message-handler/prepare.ts
    • src/slack/monitor/message-handler/prepare.thread-session-key.test.ts
  • Known bad symptoms reviewers should watch for:
    • Top-level Slack channel messages unexpectedly splitting into new sessions when replyToMode=off.

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk: Changing room session-key derivation could accidentally alter thread behavior.
    • Mitigation: Added targeted regression coverage for off/all modes and thread replies; ran focused Slack test suites.

@aisle-research-bot
Copy link

aisle-research-bot bot commented Mar 2, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Slack thread replies missing thread_ts can fall back to channel-scoped session when replyToMode=off (context bleed)

1. 🟡 Slack thread replies missing thread_ts can fall back to channel-scoped session when replyToMode=off (context bleed)

Property Value
Severity Medium
CWE CWE-200
Location src/slack/monitor/message-handler/prepare.ts:282-303

Description

In resolveSlackRoutingContext, the new roomThreadId/canonicalThreadId selection can produce no thread id for roomish chats when replyToMode === "off".

This becomes security-relevant when Slack delivers a semantic thread reply that is missing thread_ts (observed/handled elsewhere via best-effort recovery):

  • Some Slack events can include parent_user_id but omit thread_ts (see createSlackThreadTsResolver, which explicitly tries to recover missing thread_ts via conversations.history and may fail).
  • When recovery fails, resolveSlackThreadContext yields incomingThreadTs = undefined and isThreadReply = false.
  • With the new logic, for roomish messages and replyToMode="off", roomThreadId becomes undefined, so canonicalThreadId becomes undefined.
  • resolveThreadSessionKeys treats an empty/undefined thread id as no thread scoping, returning the base (channel-level) route.sessionKey.

Impact:

  • A thread reply that is missing thread_ts can be processed in the shared top-level channel session, enabling cross-thread context bleed (the agent may use context from other threads/channel turns when responding).
  • Combined with the existing reply-targeting behavior (thread targeting also depends on incomingThreadTs), this situation can also increase the likelihood of replying in the wrong place (main channel vs. thread), potentially exposing thread content more broadly.

Vulnerable code (new logic):

const roomThreadId =
  isThreadReply && threadTs
    ? threadTs
    : replyToMode === "off"
      ? undefined
      : threadContext.messageTs;
const canonicalThreadId = isRoomish
  ? roomThreadId
  : isThreadReply
    ? threadTs
    : autoThreadId;

Recommendation

Harden thread/session scoping for the known Slack edge case parent_user_id present but thread_ts missing.

Options (pick one consistent with desired UX):

  1. Treat parent_user_id as an indicator of a thread reply even when thread_ts is missing, and keep such messages isolated from the channel session (e.g., scope to message.ts as a temporary thread id):
const maybeThreadReply = Boolean(message.parent_user_id) && !threadTs;

const roomThreadId =
  isThreadReply && threadTs
    ? threadTs
    : maybeThreadReply
      ? threadContext.messageTs // isolate instead of channel-scoping
      : replyToMode === "off"
        ? undefined
        : threadContext.messageTs;
  1. Alternatively, update resolveSlackThreadContext to classify parent_user_id-only events as thread replies (and ensure the thread-ts resolver always runs before routing).

In all cases, add a regression test that simulates a roomish message with parent_user_id set, thread_ts missing, and replyToMode="off", asserting the resulting SessionKey is not channel-scoped (or that the message is dropped/retried until thread_ts is resolved).


Analyzed PR: #32193 at commit 4ebed6d

Last updated on: 2026-03-02T22:40:30Z

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR fixes a bug where Slack top-level channel messages were always assigned per-message :thread:<ts> session keys even with replyToMode=off (the default), causing the agent to lose conversational context across normal channel messages. The fix introduces a roomThreadId variable that returns undefined for non-thread room turns when replyToMode=off, allowing them to share the channel-level base session key.

Key changes:

  • prepare.ts: Replaces the single incomingThreadTs ?? message.ts fallback for room messages with a two-step logic: thread replies → threadTs, top-level with replyToMode=offundefined (channel-scoped), top-level with any other mode → messageTs (per-message-scoped, unchanged).
  • prepare.thread-session-key.test.ts: The previous test was asserting the buggy behavior (:thread: in session key for all channel messages). It is replaced with four explicit, correctly-labeled scenarios covering off/all modes and the thread-reply/DM edge cases. replyToMode is now passed consistently to both buildCtx and createSlackTestAccount, resolving a subtle prior mismatch where the context had "all" but the account defaulted to "off".

The replyToMode="first" path in rooms is functionally unchanged (still produces a per-message session via threadContext.messageTs), though no dedicated test was added for that mode — it would be a minor improvement to cover it explicitly.

Confidence Score: 5/5

  • This PR is safe to merge — the change is minimal, well-scoped, and fully covered by targeted tests.
  • The logic change is isolated to three lines of session-key derivation inside prepareSlackMessage, with no impact on outbound routing, non-Slack channels, or any auth/storage paths. The before/after behavior for thread replies, replyToMode=all, replyToMode=first, and DMs is provably unchanged by the code analysis. New tests directly verify the corrected and preserved behaviors. The only minor gap is no explicit test for replyToMode="first" in rooms, but the logic is trivially equivalent to the pre-PR behavior.
  • No files require special attention.

Last reviewed commit: b0e4203

@steipete steipete force-pushed the bm/slack-top-level-session-key branch from b0e4203 to 4ebed6d Compare March 2, 2026 22:05
@steipete steipete merged commit 2c39731 into openclaw:main Mar 2, 2026
@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: pnpm test src/slack/monitor/message-handler/prepare.thread-session-key.test.ts src/slack/monitor/message-handler/prepare.test.ts
  • Land commit: LAND_SHA_PLACEHOLDER
  • Merge commit: MERGE_SHA_PLACEHOLDER

Thanks @bmendonca3!

@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: pnpm test src/slack/monitor/message-handler/prepare.thread-session-key.test.ts src/slack/monitor/message-handler/prepare.test.ts
  • Land commit: 4ebed6d
  • Merge commit: 2c39731

Thanks @bmendonca3!

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ebed6d9d7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +295 to +297
: replyToMode === "off"
? undefined
: threadContext.messageTs;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve context when off-mode turns move into threads

With replyToMode="off", this branch makes top-level room messages use the base channel session, while thread replies still use a :thread:<thread_ts> session. That means a common flow (top-level questionassistant replyuser replies in thread) now jumps to a fresh session and loses prior assistant/user context unless thread.inheritParent is enabled (it defaults to false in src/slack/monitor/provider.ts). Before this change, both turns shared the same thread key, so continuity was preserved.

Useful? React with 👍 / 👎.

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

Labels

channel: slack Channel integration: slack size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Slack top level messages treated as new treads for every message, losing all history and context

2 participants