Skip to content

feat(slack): implement fetchThreadParent for thread-context seeding#2615

Open
apparentsoft wants to merge 1 commit into
nanocoai:channelsfrom
apparentsoft:pr/slack-thread-parent
Open

feat(slack): implement fetchThreadParent for thread-context seeding#2615
apparentsoft wants to merge 1 commit into
nanocoai:channelsfrom
apparentsoft:pr/slack-thread-parent

Conversation

@apparentsoft
Copy link
Copy Markdown

Summary

Implements the optional fetchThreadParent hook 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 ChannelAdapter and 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 existing replyTo formatter. 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:
    • Parses Slack platform/thread ids back to channel + ts.
    • Calls conversations.replies?channel=...&ts=...&limit=1 to get the top-level parent (NOT the most recent reply — Slack returns the array starting from the parent).
    • Resolves display names: users.info for human messages, bot_profile.name first then bots.info for bots.
    • Process-lifetime cache for display names — Slack Web API rate limits matter and the same names recur often.
    • 4s fetch timeout; 4000-char text truncation; fail-open everywhere.
  • 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 the log and createThreadParentFetcher imports, then assigns bridge.fetchThreadParent = createThreadParentFetcher({botToken: env.SLACK_BOT_TOKEN, log: (msg, meta) => log.warn(msg, meta)}).

Required Slack scopes

fetchThreadParent issues conversations.replies, users.info, and bots.info. The existing /add-slack scope 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

  • 13 unit tests pass against the helper in isolation.
  • After feat(router): seed thread parent into fresh per-thread sessions #2614 merges: install on a real Slack workspace, reply to a message to start a new thread, confirm the agent's first turn sees the parent as <quoted_message>.
  • Confirm a users:read-missing token still routes the message — fetcher returns null, 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.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant