Skip to content

fix(slack): restore persistent per-channel session routing#32320

Merged
steipete merged 1 commit intoopenclaw:mainfrom
AytuncYildizli:fix/slack-session-routing
Mar 3, 2026
Merged

fix(slack): restore persistent per-channel session routing#32320
steipete merged 1 commit intoopenclaw:mainfrom
AytuncYildizli:fix/slack-session-routing

Conversation

@AytuncYildizli
Copy link
Copy Markdown
Contributor

Fixes #32285

Problem

After upgrading from v2026.2.26 to v2026.3.1, Slack channel messages create isolated sessions instead of persistent per-channel sessions. Each message starts fresh with no conversation context. Downgrading to v2026.2.26 restores expected behavior.

Root Cause

Commit 11d3470 (fix(slack): use thread-level sessions for channels to prevent context mixing (#10686)) introduced per-message thread scoping for channel messages. The roomThreadId computation fell through to threadContext.messageTs (the message's own timestamp) whenever replyToMode was not "off", giving every top-level channel message its own session key (agent:…:thread:<messageTs>).

This effectively isolated every single message into its own session, breaking conversation continuity.

Fix

Only derive thread-specific session keys when the message is an actual thread reply (thread_ts present and different from the message's own ts). Top-level channel messages now stay on the per-channel session key (agent:main:slack:channel:<channelId>) regardless of replyToMode setting.

Thread replies continue to get their own sessions via the parent thread_ts, preserving the cross-thread isolation that #10686 intended.

Changes

  • src/slack/monitor/message-handler/prepare.ts: Simplified roomThreadId to only use threadTs for actual thread replies
  • src/slack/monitor/message-handler/prepare.thread-session-key.test.ts: Updated tests to assert per-channel session continuity for all replyToMode values

Testing

  • 4 dedicated thread-session-key tests pass ✅
  • 21 prepare tests pass ✅
  • 25 tool-result tests pass ✅
  • Total: 50/50 Slack tests passing

@openclaw-barnacle openclaw-barnacle bot added channel: slack Channel integration: slack size: S labels Mar 3, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 3, 2026

Greptile Summary

This PR correctly fixes a regression introduced in commit 11d34700c where every top-level Slack channel message was assigned its own session key (using its own ts as the thread id), breaking conversation continuity. The one-line fix to roomThreadId in prepare.ts ensures thread-specific session forking only happens for genuine thread replies, while all top-level channel/group messages remain on the shared per-channel session key.

  • prepare.ts: roomThreadId simplified to isThreadReply && threadTs ? threadTs : undefined, eliminating the replyToMode-dependent fallback to threadContext.messageTs that was the root cause of the regression.
  • prepare.thread-session-key.test.ts: Two tests asserting the (broken) per-message session behavior are replaced with a single parameterized test that verifies session-key continuity across all three replyToMode values for top-level channel messages.
  • The stale three-line comment above autoThreadId (lines 281-283) was originally written to describe both autoThreadId and the old roomThreadId chain; the first two lines now describe roomThreadId behavior rather than autoThreadId, and should be updated for clarity.
  • The loop-based test structure in the new test case may hinder CI triage when one specific mode regresses, since the failing mode isn't surfaced in the test name; it.each or separate it blocks per mode would be clearer.

Confidence Score: 5/5

  • This PR is safe to merge — it is a minimal, targeted fix with no risk of unintended side-effects on other message types.
  • The change is a single-expression simplification with a clear causal chain from the identified root cause to the fix. DM and thread-reply paths are untouched. The test suite is updated to cover all three replyToMode values and existing tests for thread replies and DMs still pass. Only style-level comments were raised.
  • No files require special attention.

Last reviewed commit: 7e469fd

Copy link
Copy Markdown

@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: 7e469fd0cb

ℹ️ 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 +293 to 294
const roomThreadId = isThreadReply && threadTs ? threadTs : undefined;
const canonicalThreadId = isRoomish ? roomThreadId : isThreadReply ? threadTs : autoThreadId;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep thread starters on the same session as thread replies

This change makes every top-level room message use the channel session key, so in replyToMode="all" the thread starter turn is stored under agent:...:slack:channel:<id> while the very next user reply in that same thread switches to :thread:<thread_ts>. In configurations that set channels.slack.thread.initialHistoryLimit to 0 (see src/slack/monitor/message-handler/prepare-thread-context.ts:73-80), the new thread session cannot backfill prior thread messages, so the follow-up loses the assistant context from the opener that previously lived in the same session.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +86 to 112
it("keeps top-level channel messages on the per-channel session regardless of replyToMode", async () => {
for (const mode of ["all", "first", "off"] as const) {
const ctx = buildCtx({ replyToMode: mode });
ctx.resolveUserName = async () => ({ name: "Carol" });
const account = createSlackTestAccount({ replyToMode: mode });

const first = await prepareSlackMessage({
ctx,
account,
message: buildChannelMessage({ ts: "1770408530.000000" }),
opts: { source: "message" },
});
const second = await prepareSlackMessage({
ctx,
account,
message: buildChannelMessage({ ts: "1770408531.000000" }),
opts: { source: "message" },
});

expect(first).toBeTruthy();
expect(second).toBeTruthy();
const firstKey = first!.ctxPayload.SessionKey as string;
const secondKey = second!.ctxPayload.SessionKey as string;
expect(firstKey).toBe(secondKey);
expect(firstKey).not.toContain(":thread:");
}
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Loop-based test obscures which replyToMode value failed

The three modes are exercised inside a single it block with a for loop. If the assertion fails for, say, "first", the error output won't immediately surface the offending mode — you'd need to dig into the stack trace. Consider using separate it blocks per mode (or dynamically generating them via it.each) so that CI output clearly identifies which mode regressed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/slack/monitor/message-handler/prepare.thread-session-key.test.ts
Line: 86-112

Comment:
**Loop-based test obscures which `replyToMode` value failed**

The three modes are exercised inside a single `it` block with a `for` loop. If the assertion fails for, say, `"first"`, the error output won't immediately surface the offending mode — you'd need to dig into the stack trace. Consider using separate `it` blocks per mode (or dynamically generating them via `it.each`) so that CI output clearly identifies which mode regressed.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 3, 2026

Additional Comments (1)

src/slack/monitor/message-handler/prepare.ts
Stale comment now describes roomThreadId, not autoThreadId

The three-line comment block immediately above autoThreadId was originally written to capture the combined intent of autoThreadId and the old roomThreadId chain. After this PR, the first two lines — "Keep true thread replies thread-scoped, but preserve channel-level sessions for top-level room turns when replyToMode is off" — describe the new roomThreadId logic, not autoThreadId (which is exclusively used for DMs). Leaving this comment attached to autoThreadId will confuse future readers about what that variable actually controls.

Consider splitting the comment so each variable is described individually, e.g.:

  // For DMs with replyToMode="all", create a per-message auto-thread so each
  // top-level DM turn gets its own isolated session.
  const autoThreadId =
    !isThreadReply && replyToMode === "all" && threadContext.messageTs
      ? threadContext.messageTs
      : undefined;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/slack/monitor/message-handler/prepare.ts
Line: 281-287

Comment:
**Stale comment now describes `roomThreadId`, not `autoThreadId`**

The three-line comment block immediately above `autoThreadId` was originally written to capture the combined intent of `autoThreadId` *and* the old `roomThreadId` chain. After this PR, the first two lines — *"Keep true thread replies thread-scoped, but preserve channel-level sessions for top-level room turns when replyToMode is off"* — describe the new `roomThreadId` logic, not `autoThreadId` (which is exclusively used for DMs). Leaving this comment attached to `autoThreadId` will confuse future readers about what that variable actually controls.

Consider splitting the comment so each variable is described individually, e.g.:

```suggestion
  // For DMs with replyToMode="all", create a per-message auto-thread so each
  // top-level DM turn gets its own isolated session.
  const autoThreadId =
    !isThreadReply && replyToMode === "all" && threadContext.messageTs
      ? threadContext.messageTs
      : undefined;
```

How can I resolve this? If you propose a fix, please make it concise.

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
@AytuncYildizli AytuncYildizli force-pushed the fix/slack-session-routing branch from 7e469fd to d0aee39 Compare March 3, 2026 00:06
Copy link
Copy Markdown

@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: d0aee39807

ℹ️ 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".

// Top-level channel messages must stay on the per-channel session for continuity.
// Before this fix, every channel message used its own ts as threadId, creating
// isolated sessions per message (regression from #10686).
const roomThreadId = isThreadReply && threadTs ? threadTs : undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Start inherited thread sessions on the starter message

With channels.slack.thread.inheritParent=true, this change makes top-level channel messages skip thread session creation (roomThreadId is now undefined unless it is already a thread reply), so the thread session is created later on the first reply. At that point, ParentSessionKey forking in auto-reply/reply/session.ts copies the current channel transcript, which can include unrelated messages posted after the thread started, causing cross-thread context leakage in busy channels; previously the fork happened on the starter turn and used a stable snapshot.

Useful? React with 👍 / 👎.

@steipete steipete merged commit a448435 into openclaw:main Mar 3, 2026
26 checks passed
@steipete
Copy link
Copy Markdown
Contributor

steipete commented Mar 3, 2026

Landed.

  • Gate: pnpm vitest run src/slack/monitor/message-handler/prepare.thread-session-key.test.ts src/slack/monitor/message-handler/prepare.test.ts
  • Merge commit: a448435

Thanks @AytuncYildizli!

newtontech pushed a commit to newtontech/openclaw-fork that referenced this pull request Mar 3, 2026
…2353)

Adds explicit test coverage for the edge case where Slack auto-sets
thread_ts == ts on top-level messages. This was fixed in openclaw#32320 but
lacked a specific regression test for this exact scenario.

The test verifies that messages with thread_ts == ts do NOT get a
:thread: suffix in their session key, ensuring they stay on the
per-channel session for continuity.

Related: openclaw#32320, openclaw#32285
planfit-alan pushed a commit to planfit/openclaw that referenced this pull request Mar 3, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
dawi369 pushed a commit to dawi369/davis that referenced this pull request Mar 3, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
OWALabuy pushed a commit to kcinzgg/openclaw that referenced this pull request Mar 4, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
AytuncYildizli added a commit to AytuncYildizli/openclaw that referenced this pull request Mar 4, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 19, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285

(cherry picked from commit a448435)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 19, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285

(cherry picked from commit a448435)
ephb pushed a commit to ephb-bot/openclaw that referenced this pull request Mar 19, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
lukeg826 pushed a commit to lukeg826/openclaw that referenced this pull request Mar 26, 2026
…32320)

Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in openclaw#10686, every new channel message got its own session key (agent:...:thread:<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes openclaw#32285
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]: Version 2026.3.1 breaks Slack session routing

2 participants