44 * Channels self-register on import. The host calls initChannelAdapters() at startup
55 * to instantiate and set up all registered adapters.
66 */
7- import type { ChannelAdapter , ChannelRegistration , ChannelSetup } from './adapter.js' ;
7+ import type { ChannelAdapter , ChannelRegistration , ChannelSetup , OutboundFile } from './adapter.js' ;
8+ import type { ChannelDeliveryAdapter } from '../delivery.js' ;
89import { log } from '../log.js' ;
910
1011const SETUP_RETRY_DELAYS_MS = [ 2000 , 5000 , 10000 ] ;
@@ -18,14 +19,6 @@ function isNetworkError(err: unknown): err is Error {
1819
1920const sleep = ( ms : number ) => new Promise < void > ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
2021
21- /**
22- * Two gateway instances of one platform (e.g. two Discord bots) identifying
23- * simultaneously from one IP trip platform rate limits at boot. Stagger the
24- * second (and later) same-platform adapter's setup. Inert for installs with
25- * one adapter per platform — no two registrations share a channelType.
26- */
27- const SAME_CHANNEL_SETUP_STAGGER_MS = 10_000 ;
28-
2922const registry = new Map < string , ChannelRegistration > ( ) ;
3023const activeAdapters = new Map < string , ChannelAdapter > ( ) ;
3124
@@ -34,22 +27,81 @@ export function registerChannelAdapter(name: string, registration: ChannelRegist
3427 registry . set ( name , registration ) ;
3528}
3629
30+ /** Get a live adapter by its EXACT registry key (instance name; default
31+ * instances are keyed by channelType itself). No channelType fallback —
32+ * callers that address a specific instance (outbound delivery, typing)
33+ * must never be rerouted through a sibling instance: that would send
34+ * through the wrong bot identity with the wrong token. A missing key
35+ * means the owning adapter is offline; callers apply their normal
36+ * offline-adapter handling. */
37+ export function getChannelAdapterExact ( key : string ) : ChannelAdapter | undefined {
38+ return activeAdapters . get ( key ) ;
39+ }
40+
3741/** Get a live adapter by instance name, falling back to any adapter of the
38- * given channel type. channelType-only callers (user-id prefix resolution
39- * and cold DMs in user-dm.ts, approval delivery in channel-approval.ts)
40- * must still resolve when every instance of a platform is named — first
41- * registered wins (Map insertion order, deterministic). Default instances
42- * are keyed by channelType itself, so single-instance installs always hit
43- * the exact-key path. */
42+ * given channel type. The fallback exists ONLY for channelType-only callers
43+ * (user-id prefix resolution and cold DMs in user-dm.ts, approval delivery
44+ * in channel-approval.ts, the router's thread-policy probe when an event
45+ * carries no instance) — they must still resolve when every instance of a
46+ * platform is named. First registered wins (Map insertion order,
47+ * deterministic). Default instances are keyed by channelType itself, so
48+ * single-instance installs always hit the exact-key path. Instance-addressed
49+ * dispatch (delivery, typing) must use getChannelAdapterExact instead. */
4450export function getChannelAdapter ( key : string ) : ChannelAdapter | undefined {
4551 const exact = activeAdapters . get ( key ) ;
4652 if ( exact ) return exact ;
47- for ( const adapter of activeAdapters . values ( ) ) {
48- if ( adapter . channelType === key ) return adapter ;
53+ for ( const [ registryKey , adapter ] of activeAdapters ) {
54+ if ( adapter . channelType === key ) {
55+ log . warn ( 'Channel adapter fallback: requested key resolved through a differently-keyed instance' , {
56+ requested : key ,
57+ resolvedKey : registryKey ,
58+ } ) ;
59+ return adapter ;
60+ }
4961 }
5062 return undefined ;
5163}
5264
65+ /**
66+ * Build the host's outbound delivery bridge: dispatches delivery-poll and
67+ * typing traffic into the adapter registry. Resolution is EXACT-key only —
68+ * `instance ?? channelType`. For default-instance messaging_groups rows the
69+ * stored instance IS the channelType, which matches default-registered
70+ * adapters, so single-instance behavior is unchanged. A named instance whose
71+ * adapter is offline gets the normal offline-adapter handling (warn + drop
72+ * into the delivery retry path) — never a cross-identity send through a
73+ * sibling bot of the same platform.
74+ */
75+ export function createChannelDeliveryAdapter ( ) : ChannelDeliveryAdapter {
76+ return {
77+ async deliver (
78+ channelType : string ,
79+ platformId : string ,
80+ threadId : string | null ,
81+ kind : string ,
82+ content : string ,
83+ files ?: OutboundFile [ ] ,
84+ instance ?: string ,
85+ ) : Promise < string | undefined > {
86+ const adapter = getChannelAdapterExact ( instance ?? channelType ) ;
87+ if ( ! adapter ) {
88+ log . warn ( 'No adapter for channel type' , { channelType, instance } ) ;
89+ return ;
90+ }
91+ return adapter . deliver ( platformId , threadId , { kind, content : JSON . parse ( content ) , files } ) ;
92+ } ,
93+ async setTyping (
94+ channelType : string ,
95+ platformId : string ,
96+ threadId : string | null ,
97+ instance ?: string ,
98+ ) : Promise < void > {
99+ const adapter = getChannelAdapterExact ( instance ?? channelType ) ;
100+ await adapter ?. setTyping ?.( platformId , threadId ) ;
101+ } ,
102+ } ;
103+ }
104+
53105/** Get all active adapters. */
54106export function getActiveAdapters ( ) : ChannelAdapter [ ] {
55107 return [ ...activeAdapters . values ( ) ] ;
@@ -70,7 +122,6 @@ export function getChannelContainerConfig(name: string): ChannelRegistration['co
70122 * Skips adapters that return null (missing credentials).
71123 */
72124export async function initChannelAdapters ( setupFn : ( adapter : ChannelAdapter ) => ChannelSetup ) : Promise < void > {
73- const activeChannelTypes = new Set < string > ( ) ;
74125 for ( const [ name , registration ] of registry ) {
75126 try {
76127 const adapter = await registration . factory ( ) ;
@@ -79,17 +130,6 @@ export async function initChannelAdapters(setupFn: (adapter: ChannelAdapter) =>
79130 continue ;
80131 }
81132
82- // Same-platform stagger: a second instance of an already-active
83- // platform waits before identifying (gateway logins from one IP).
84- if ( activeChannelTypes . has ( adapter . channelType ) ) {
85- log . info ( 'Staggering same-platform adapter setup' , {
86- channel : name ,
87- type : adapter . channelType ,
88- delayMs : SAME_CHANNEL_SETUP_STAGGER_MS ,
89- } ) ;
90- await sleep ( SAME_CHANNEL_SETUP_STAGGER_MS ) ;
91- }
92-
93133 const setup = setupFn ( adapter ) ;
94134 // Transient network failures during adapter init (e.g. Telegram deleteWebhook
95135 // hitting a DNS hiccup at boot) would otherwise leave the channel permanently
@@ -125,7 +165,6 @@ export async function initChannelAdapters(setupFn: (adapter: ChannelAdapter) =>
125165 log . warn ( 'Duplicate adapter instance key — overwriting previous adapter' , { key, channel : name } ) ;
126166 }
127167 activeAdapters . set ( key , adapter ) ;
128- activeChannelTypes . add ( adapter . channelType ) ;
129168 log . info ( 'Channel adapter started' , { channel : name , type : adapter . channelType , instance : key } ) ;
130169 } catch ( err ) {
131170 log . error ( 'Failed to start channel adapter' , { channel : name , err } ) ;
0 commit comments