6.0 KiB
title, sidebarTitle
| title | sidebarTitle |
|---|---|
| Channel route unification refactor | Channel route unification |
Channel route unification refactor
This is a temporary implementation plan. Delete this file before merging the refactor PR after the code, tests, and PR description prove the plan is complete.
Problem
Channel routing is represented several times:
ChannelRouteRefis the SDK route identity shape.ChannelOutboundSessionRouteis the executable session route returned by channel messaging adapters.ConversationRefandSessionBindingRecordidentify bound conversations.DeliveryContextmirrors route fields for sessions, sentinels, tools, and protocol compatibility.SessionEntrystoresdeliveryContext,lastChannel,lastTo,lastAccountId, andlastThreadIdas overlapping last-route fields.
The Discord subagent thread bug happened because a plugin hook had to return a
second ad hoc route object after core already knew a binding existed. TypeScript
could not protect that path because the hook result made deliveryOrigin
optional and core treated it as a best-effort delivery hint.
Goals
- Make
ChannelRouteRefthe canonical route metadata shape inside core. - Keep
ChannelOutboundSessionRouteas the channel-owned executable session route. - Keep
ConversationRefas binding identity, not as a send target. - Treat
DeliveryContextas compatibility projection, not core routing truth. - Share binding-to-route projection between subagent and ACP spawn paths.
- Keep sends in the durable message pipeline with
threadIdandreplyToId. - Deprecate ad hoc plugin hook fields once bundled callers use core projection.
Non-goals
- Do not add a new route type family.
- Do not make core create provider-native threads directly.
- Do not infer child-thread support from a provider having any thread concept.
- Do not remove persisted session compatibility fields in the first pass.
- Do not bypass channel message adapters or durable final delivery.
Existing contracts to keep
ChannelRouteRef
src/plugin-sdk/channel-route.ts owns route normalization and matching. Extend
helpers here only when the concept belongs in the SDK surface. Core-only
projection helpers should live outside the SDK.
ChannelOutboundSessionRoute
src/channels/plugins/types.core.ts owns the route returned from
messaging.resolveOutboundSessionRoute. Plugins keep provider-native parsing
rules here and should use buildChannelOutboundSessionRoute or
buildThreadAwareOutboundSessionRoute.
ConversationRef
src/infra/outbound/session-binding.types.ts owns binding identity. A
conversation can require plugin resolution before it is routable.
DeliveryContext
src/utils/delivery-context.types.ts remains for protocol, session, sentinel,
and tool compatibility. New core code should convert it to ChannelRouteRef
before comparing or carrying route state.
Implementation phases
Phase 1: route projection helpers
Add a core route projection module with helpers that:
- narrow a
ChannelRouteRefto a routable route withchannelandtarget.to; - project
DeliveryContextto and fromChannelRouteRef; - project
SessionEntryroute fields toChannelRouteRef; - project
ConversationRefto a route through pluginresolveDeliveryTargetwith the existing genericchannel:<conversationId>fallback; - project
SessionBindingRecordto a route using itsconversation.
Tests must cover:
- normalized channel/account/to/thread fields;
- generic fallback routing when a plugin has no delivery-target projection;
- parent/child thread projection for Slack-like targets;
- same-channel merge without crossing fields between unrelated channels.
Phase 2: route-first session compatibility
Add a canonical optional route field to session entries, then update session delivery helpers to read the route first and derive legacy fields from it. Keep writing legacy fields for compatibility.
Tests must cover:
- route wins over legacy fields when present;
- old session entries still hydrate;
- existing subagent session delivery context is not overwritten by spawn request params.
Phase 3: shared spawn route planner
Move subagent and ACP binding route construction into a shared planner. The planner returns:
- requester route;
- binding record;
- child delivery route when routable;
- compatibility delivery context;
- whether inline child delivery is allowed.
Subagent and ACP callers must stop open-coding binding-to-delivery projection.
Tests must cover:
- Discord-style child thread delivery;
- Slack-style parent channel plus thread id delivery;
- current-conversation binding without routable child delivery;
- requester origin kept separate from child delivery route.
Phase 4: bundled plugin hook deprecation path
Move bundled plugins toward core binding projection:
subagent_spawning.deliveryOriginbecomes deprecated compatibility output.subagent_spawning.threadBindingReadybecomes deprecated compatibility readiness.subagent_delivery_targetbecomes deprecated once core can resolve the bound delivery route throughresolveDeliveryTarget.
Keep public SDK compatibility during the transition and document deprecations in types and PR notes.
Phase 5: channel route adapter cleanup
Normalize bundled channel route builders:
- Discord and Slack stay on
buildThreadAwareOutboundSessionRoute. - Feishu, Matrix, and other channel route builders use shared route builders where possible.
- MS Teams keeps normal thread send support separate from child-thread binding
until durable final
threadcapability and binding placement are explicitly proven.
Validation checklist
- Focused unit tests for route projection.
- Subagent thread-binding tests.
- ACP spawn route tests.
- Slack, Discord, Feishu, Matrix, and MS Teams channel route tests where touched.
pnpm check:changedor Testbox equivalent before PR.- Autoreview until no accepted actionable findings remain.
- PR description includes deprecations, compatibility behavior, and proof.
- Delete this file after the PR is green and the description is complete.