feat(slack): implement fetchThreadParent for thread-context seeding#2615
Open
apparentsoft wants to merge 1 commit into
Open
feat(slack): implement fetchThreadParent for thread-context seeding#2615apparentsoft wants to merge 1 commit into
apparentsoft wants to merge 1 commit into
Conversation
Implements the optional `fetchThreadParent` hook that the router introduces in nanocoai#2614. When a user starts a thread by replying to some earlier message in a Slack channel, and that thread's first reply creates a new per-thread session, the router will now seed the originating top-level message into the agent's `replyTo` field instead of leaving the agent guessing. How it works `slack-thread-parent.ts` adds `createThreadParentFetcher({botToken, log})` which the adapter wires into the bridge as `bridge.fetchThreadParent`. The fetcher: - Parses Slack platform/thread ids back to channel id + ts. - Calls `conversations.replies` with `limit=1` to get the top-level parent message (NOT the most recent reply). - Resolves the sender display name via `users.info` for human messages or via `bot_profile.name` / `bots.info` for bot/app messages. - Caches display names for the host process lifetime — same name lookups recur often and Slack rate-limits Web API calls. - Truncates message text to 4000 chars defensively. - 4s fetch timeout per call. - Fails open at every step (network error, missing scopes, bot tokens without `users:read`, deleted parent, unparseable ids) — returns `null` and the router falls through to its no-context path. Dependency Requires nanocoai#2614 (the framework hook on `ChannelAdapter`). Without it, the line `bridge.fetchThreadParent = ...` won't typecheck. Please merge nanocoai#2614 first. Tested 13 unit tests covering id parsers, name resolution paths (user, bot with bot_profile, bot via bots.info), cache hits, timeout behavior, and fail-open semantics.
This was referenced May 26, 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.
Summary
Implements the optional
fetchThreadParenthook on the Slack adapter so the router can seed a fresh per-thread session's first inbound with the originating top-level message of the thread.Depends on #2614 (the framework hook on
ChannelAdapterand the router's seeding logic). This PR will not typecheck until #2614 lands — please merge #2614 first.User-facing behavior
Without this PR: when a user replies to an existing Slack message and starts a new thread, the bot's first session in that thread sees only the reply text. It has no idea what message is being replied to.
With this PR (and #2614): the agent's first turn sees the originating top-level message of the thread rendered as
<quoted_message>via the container's existingreplyToformatter. No more "what are you talking about?" moments on thread starts.What changed
src/channels/slack-thread-parent.ts(new, 141 lines) —createThreadParentFetcher({botToken, log})returns a(platformId, threadId) => Promise<{id, sender, text} | null>. Internals:channel+ts.conversations.replies?channel=...&ts=...&limit=1to get the top-level parent (NOT the most recent reply — Slack returns the array starting from the parent).users.infofor human messages,bot_profile.namefirst thenbots.infofor bots.src/channels/slack-thread-parent.test.ts(new, 180 lines) — 13 tests covering id parsers, all three name-resolution paths, cache hits, timeout, and every fail-open branch.src/channels/slack.ts(+6 lines) — adds thelogandcreateThreadParentFetcherimports, then assignsbridge.fetchThreadParent = createThreadParentFetcher({botToken: env.SLACK_BOT_TOKEN, log: (msg, meta) => log.warn(msg, meta)}).Required Slack scopes
fetchThreadParentissuesconversations.replies,users.info, andbots.info. The existing/add-slackscope list (channels:history,groups:history,im:history,users:read) already covers all of these — no scope change needed for the install instructions.Test plan for review
<quoted_message>.users:read-missing token still routes the message — fetcher returnsnull, formatter omits the quote, agent sees the bare reply (today's behavior).Compatibility
No breaking changes. No schema/DB changes. No env changes. The implementation only activates when paired with the framework PR; with that absent, this code wouldn't compile, but that's the same as saying it can't be merged without its dependency.