feat(router): per-channel threadId rewrite for per-thread sessions#2471
Open
luisfontes wants to merge 1 commit into
Open
feat(router): per-channel threadId rewrite for per-thread sessions#2471luisfontes wants to merge 1 commit into
luisfontes wants to merge 1 commit into
Conversation
3 tasks
Add an optional `rewriteThreadIdForSession` hook on the ChannelAdapter interface. The router calls it inside `deliverToAgent` only when the wiring's effective session_mode is per-thread, applying it to both the session key and the inbound's deliveryAddr threadId so inbound / session / outbound all agree on the same id. Why opt-in via this hook: some platforms encode top-level (un-threaded) messages with an ambiguous "empty thread" placeholder. A Slack DM event without a pre-existing thread arrives with `thread_ts=""`, so every top-level DM collapses to one threadId and `session_mode=per-thread` on a DM wiring degenerates to a single ever-growing session. The adapter that knows the platform's encoding can mint a per-message id; the router stays adapter-agnostic. The hook is consulted ONLY in per-thread mode because shared / agent- shared sessions don't gate on threadId — rewriting in those modes would just point outbound replies somewhere different without changing routing. INVARIANT documented on the hook: `channelIdFromThreadId(rewritten)` must equal `channelIdFromThreadId(original)`. The router doesn't re-derive channelId after rewrite; the adapter must preserve it. Slack uses this in a follow-up PR against the `channels` branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ab3d314 to
08ffd42
Compare
This was referenced May 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
No user-facing change in this PR. Adds an optional adapter hook that the router applies only in
session_mode=per-thread, so channel adapters can fix platforms (Slack DMs) where the SDK assigns the same threadId to every top-level message andper-threadconsequently collapses to one session. Slack consumes it in #2472.Summary
Adds an optional
rewriteThreadIdForSessionhook on theChannelAdapterinterface. The router calls it insidedeliverToAgentonly when the wiring's effectivesession_modeisper-thread, applying it to both the session key and the inbound'sdeliveryAddr.threadIdso inbound / session / outbound all agree on the same thread id.Why
Some platforms encode top-level (un-threaded) messages with an ambiguous "empty thread" placeholder that collapses every top-level post to the same threadId. A concrete example, which motivated this PR: in Slack, a top-level DM event arrives with
thread_ts="", so every unsolicited DM gets the same threadId. A wiring withsession_mode=per-threadon that DM degenerates to one ever-growing session, and outbound replies post as flat channel messages instead of in-thread (becausepostMessagereadsthread_tsfrom the threadId, and empty means "post at top level").The adapter knows the platform's encoding and can mint a per-message id; the router stays adapter-agnostic.
Why opt-in, and why only in per-thread mode
effectiveSessionMode === 'per-thread'. Inshared/agent-sharedmode the threadId doesn't gate the session, so a rewrite wouldn't help and isn't called.channelIdFromThreadId(rewritten)must equalchannelIdFromThreadId(original). The router doesn't re-derivechannelIdafter rewrite, so the adapter is responsible for preserving it.Result: this is byte-identical for every existing install. Operators who explicitly opt into per-thread DM sessions get correct behavior — no env var, no new config; just follows from the existing
session_modeknob.Files
src/channels/adapter.ts— addsrewriteThreadIdForSession?(threadId, messageId)toChannelAdapter, with INVARIANT note.src/router.ts— computeseffectiveThreadIdand threads it intoresolveSessionanddeliveryAddr.Test plan
pnpm run build,pnpm test).rewriteThreadIdForSessionin this PR → behavior unchanged for all current channels.channelsbranch wires the Slack adapter to this hook. Verified locally by combining both patches: top-level Slack DM undersession_mode=per-threadproduces a fresh session per message; outbound reply posts in-thread.🤖 Generated with Claude Code