Skip to content

feat(router): seed thread parent into fresh per-thread sessions#2614

Open
apparentsoft wants to merge 1 commit into
nanocoai:mainfrom
apparentsoft:pr/channel-adapter-fetch-thread-parent
Open

feat(router): seed thread parent into fresh per-thread sessions#2614
apparentsoft wants to merge 1 commit into
nanocoai:mainfrom
apparentsoft:pr/channel-adapter-fetch-thread-parent

Conversation

@apparentsoft
Copy link
Copy Markdown

Summary

Adds an optional fetchThreadParent hook on ChannelAdapter, 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's replyTo field so the container's formatter renders it as <quoted_message> in the agent's view.

Framework only. No adapter implements fetchThreadParent in this PR — without an implementation the behavior is byte-identical to today. The Slack implementation lands in a follow-up PR against the channels branch 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 replyTo gives 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 optional fetchThreadParent?(platformId, threadId): Promise<{id, sender, text} | null> to the ChannelAdapter interface, with doc on fail-open semantics.
  • src/router.ts — two pieces:
    1. Memoized fetcher at the top of routeInbound: lazy, cached per event, fail-open (a throw or null collapses to "no parent"). Avoids redundant platform API calls when multiple agents wired to the same channel each create a new session.
    2. Seeding block at the top of deliverToAgent: fires only when created && event.threadId && adapterSupportsThreads. Parses the inbound content as JSON; if it's not JSON or already has replyTo, skips. Otherwise sets replyTo = {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

Condition Behavior
Adapter doesn't implement fetchThreadParent No-op
Adapter throws Logged warn, treated as null
Returns null No seeding
Inbound is not JSON Skipped (legacy/native adapters)
Inbound already has replyTo Not overwritten
Parent id === inbound id Not seeded (single-message thread)
Not the first message of the session (created=false) Not seeded
Not a threaded adapter Not seeded
Inbound has no threadId (DM) Not seeded

Test plan for review

  • pnpm run build clean
  • pnpm test — 332/332 pass on this branch
  • Reviewer: visual diff confirms deliverToAgent signature change is consistent at both call sites in routeInbound (engaged and accumulate paths both pass getThreadParent).
  • Reviewer: confirm no adapter currently in trunk implements fetchThreadParent (it doesn't — channels live on the channels branch only).

Follow-up PR

Slack implementation (src/channels/slack-thread-parent.ts + wiring in src/channels/slack.ts) against the channels branch. 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.

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)
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