feat(router): seed thread parent into fresh per-thread sessions#2614
Open
apparentsoft wants to merge 1 commit into
Open
feat(router): seed thread parent into fresh per-thread sessions#2614apparentsoft wants to merge 1 commit into
apparentsoft wants to merge 1 commit into
Conversation
Adds an optional `fetchThreadParent` hook on `ChannelAdapter` and wires the router to call it once per inbound event (memoized across all agents wired to the same channel) when a brand-new per-thread session is being created. The fetched parent is stored on the inbound's `replyTo` field so the container's formatter can render it as `<quoted_message>` in the agent's view. Why this matters When a user starts a Slack/Discord thread by replying to some earlier message and that thread's first reply triggers a new per-thread session, the agent currently wakes up with no view of what was being replied to. The agent sees only the reply text, with no context for why the user is saying it. Seeding the originating top-level message into `replyTo` gives the agent the missing context on its very first turn. Design notes - `ChannelAdapter.fetchThreadParent?` is optional. Adapters that don't implement it leave the existing behavior unchanged — agents wake with no parent context, exactly as today. - Memoization is per `routeInbound` call: when multiple agents are wired to the same channel, only one platform API call is made even if more than one new session is created in the same event. - Fail-open at every step. A throw from the adapter, a null return, a parent whose id equals the inbound's id (single-message thread), non-JSON content, or already-present `replyTo` — any of these leaves the inbound's content byte-identical to today. - Only fires when `created && event.threadId && adapterSupportsThreads`, so DMs and shared sessions are never touched. - Slack implementation lives on the channels branch and is the subject of a follow-up PR that depends on this one merging first. Tested - pnpm run build (clean) - pnpm test (332/332 pass on this branch)
3 tasks
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
Adds an optional
fetchThreadParenthook onChannelAdapter, plus router wiring that calls it once per inbound event (memoized) when a fresh per-thread session is being created. The fetched parent message is stored on the inbound'sreplyTofield so the container's formatter renders it as<quoted_message>in the agent's view.Framework only. No adapter implements
fetchThreadParentin this PR — without an implementation the behavior is byte-identical to today. The Slack implementation lands in a follow-up PR against thechannelsbranch that depends on this merging first.Motivation
Today, when a user starts a Slack/Discord thread by replying to some earlier message, and that thread's first reply triggers a new per-thread session, the agent wakes up with no view of what was being replied to. It sees only the reply text in isolation, with no context for why the user is saying it.
Seeding the originating top-level message into
replyTogives the agent the missing context on its very first turn, without changing anything for adapters that don't opt in.What changed
src/channels/adapter.ts— adds optionalfetchThreadParent?(platformId, threadId): Promise<{id, sender, text} | null>to theChannelAdapterinterface, with doc on fail-open semantics.src/router.ts— two pieces:routeInbound: lazy, cached per event, fail-open (a throw ornullcollapses to "no parent"). Avoids redundant platform API calls when multiple agents wired to the same channel each create a new session.deliverToAgent: fires only whencreated && event.threadId && adapterSupportsThreads. Parses the inbound content as JSON; if it's not JSON or already hasreplyTo, skips. Otherwise setsreplyTo = {id, sender, text}from the fetched parent, but only when the parent's id differs from the inbound's id (so single-message threads don't get a self-quote).Safety properties
fetchThreadParentnullnullreplyTocreated=false)threadId(DM)Test plan for review
pnpm run buildcleanpnpm test— 332/332 pass on this branchdeliverToAgentsignature change is consistent at both call sites inrouteInbound(engagedandaccumulatepaths both passgetThreadParent).fetchThreadParent(it doesn't — channels live on thechannelsbranch only).Follow-up PR
Slack implementation (
src/channels/slack-thread-parent.ts+ wiring insrc/channels/slack.ts) against thechannelsbranch. I'll open it as soon as this lands so reviewers can see the actual user-facing behavior end-to-end.Compatibility
No breaking changes. No schema/DB changes. No env changes. Interface addition is optional.