import { createHash, createHmac, randomUUID, timingSafeEqual } from 'node:crypto'; import { ChannelDeliveryError } from '@mosaicstack/types'; import type { ChannelAdapterHealthDto, ChannelAuthorizedPrincipalDto, ChannelConversationRouteDto, ChannelEgressDto, ChannelEgressPort, ChannelIngressDto, ChannelIngressPort, ChannelMessageDto, OfficialChannelAdapter, } from '@mosaicstack/types'; import { ChannelType, Client, GatewayIntentBits, ThreadAutoArchiveDuration, type Message as DiscordMessage, } from 'discord.js'; import { io, type Socket } from 'socket.io-client'; export interface DiscordPluginDependencies { ingressPort?: ChannelIngressPort; client?: Client; socketFactory?: (url: string, options: Parameters[1]) => Socket; } export interface DiscordPluginConfig { token: string; gatewayUrl: string; /** Maximum authorized turns per Discord user/channel per minute. */ messageRateLimitPerMinute?: number; /** Maximum mention-triggered thread routes per Discord user/channel per minute. */ threadRateLimitPerMinute?: number; /** Shared service credential injected by the approved secret mechanism. */ serviceToken: string; /** Which guild to bind to (single-guild only for v0.1.0). */ guildId?: string; allowedGuildIds: readonly string[]; allowedChannelIds: readonly string[]; allowedUserIds: readonly string[]; /** Provisioned interaction bindings; instance identity is configuration, never code. */ interactionBindings?: readonly DiscordInteractionBinding[]; } export type DiscordInteractionOperation = 'bind' | 'attach' | 'send' | 'approve' | 'stop'; export type DiscordInteractionRole = 'viewer' | 'operator' | 'admin'; /** A provisioned Discord-to-Mosaic identity pairing. */ export interface DiscordInteractionUserBinding { role: DiscordInteractionRole; /** Required for privileged approval and stop operations. */ mosaicUserId?: string; } /** Legacy role-only pairings remain valid for non-privileged Discord ingress. */ export type DiscordInteractionPairing = DiscordInteractionRole | DiscordInteractionUserBinding; export interface DiscordInteractionBinding { /** Stable logical agent identity used in channel conversation routes. */ instanceId: string; /** Trusted gateway database agent-config ID selected for this binding. */ agentConfigId: string; guildId: string; channelId: string; /** Pairing roster keyed by Discord user ID. */ pairedUsers: Readonly>; } const DEFAULT_MESSAGE_RATE_LIMIT_PER_MINUTE = 30; const DEFAULT_THREAD_RATE_LIMIT_PER_MINUTE = 5; const RATE_LIMIT_WINDOW_MS = 60_000; const DELIVERY_MAX_ATTEMPTS = 3; const DELIVERY_RETRY_BASE_MS = 50; const MAX_CONVERSATION_ROUTES = 1_000; const operationRoles: Readonly< Record > = { bind: ['admin'], attach: ['operator', 'admin'], send: ['operator', 'admin'], approve: ['admin'], stop: ['admin'], }; /** Resolves a configuration-owned binding and applies pairing/RBAC before ingress. */ export function resolveDiscordInteractionBinding( bindings: readonly DiscordInteractionBinding[], guildId: string, channelId: string, userId: string, operation: DiscordInteractionOperation, ): DiscordInteractionBinding | null { const binding = bindings.find( (candidate) => candidate.guildId === guildId && candidate.channelId === channelId, ); if (!binding) return null; const pairing = binding.pairedUsers[userId]; const role = typeof pairing === 'string' ? pairing : pairing?.role; return role && operationRoles[operation].includes(role) ? binding : null; } /** Resolves the provisioned Mosaic identity for an already-authorized Discord user. */ export function resolveDiscordInteractionActorId( binding: DiscordInteractionBinding, discordUserId: string, ): string | null { const pairing = binding.pairedUsers[discordUserId]; if (typeof pairing === 'string') return null; const mosaicUserId = pairing?.mosaicUserId?.trim(); return mosaicUserId || null; } /** Parses provisioned binding roster JSON and rejects malformed or empty data. */ export function parseDiscordInteractionBindings( value: string | undefined, ): DiscordInteractionBinding[] { if (!value) throw new Error('DISCORD_INTERACTION_BINDINGS is required when Discord is enabled'); const parsed: unknown = JSON.parse(value); if (!Array.isArray(parsed) || parsed.length === 0) { throw new Error('DISCORD_INTERACTION_BINDINGS must be a non-empty JSON array'); } return parsed.map((binding: unknown): DiscordInteractionBinding => { if (typeof binding !== 'object' || binding === null) throw new Error('Invalid Discord interaction binding'); const candidate = binding as Partial; if ( !candidate.instanceId || !candidate.agentConfigId || !candidate.guildId || !candidate.channelId || !candidate.pairedUsers || typeof candidate.pairedUsers !== 'object' ) { throw new Error('Invalid Discord interaction binding'); } const pairedUsers = Object.fromEntries( Object.entries(candidate.pairedUsers).map(([discordUserId, pairing]: [string, unknown]) => { if (!discordUserId.trim()) { throw new Error('Invalid Discord interaction user binding'); } if (typeof pairing === 'string') { if (!['viewer', 'operator', 'admin'].includes(pairing)) { throw new Error('Invalid Discord interaction user binding'); } return [discordUserId, pairing]; } if (typeof pairing !== 'object' || pairing === null) { throw new Error('Invalid Discord interaction user binding'); } const userBinding = pairing as Partial; if ( !userBinding.role || !['viewer', 'operator', 'admin'].includes(userBinding.role) || (userBinding.mosaicUserId !== undefined && !userBinding.mosaicUserId.trim()) ) { throw new Error('Invalid Discord interaction user binding'); } return [ discordUserId, { role: userBinding.role, ...(userBinding.mosaicUserId ? { mosaicUserId: userBinding.mosaicUserId } : {}), }, ]; }), ) as Record; return { instanceId: candidate.instanceId, agentConfigId: candidate.agentConfigId, guildId: candidate.guildId, channelId: candidate.channelId, pairedUsers, }; }); } export interface DiscordIngressPayload { correlationId: string; messageId: string; guildId: string; channelId: string; userId: string; conversationId: string; content: string; threadId?: string; attachments?: readonly DiscordAttachment[]; } export interface DiscordAttachment { id: string; name: string; url: string; contentType: string | null; sizeBytes?: number; } export interface DiscordIngressEnvelope { payload: DiscordIngressPayload; signature: string; } export interface DiscordIngressAllowlists { guildIds: readonly string[]; channelIds: readonly string[]; userIds: readonly string[]; } function signedPayload(payload: DiscordIngressPayload): string { return [ payload.correlationId, payload.messageId, payload.guildId, payload.channelId, payload.userId, payload.conversationId, payload.content, payload.threadId ?? '', JSON.stringify(payload.attachments ?? []), ].join('\n'); } function signPayload(payload: DiscordIngressPayload, serviceToken: string): string { return createHmac('sha256', serviceToken).update(signedPayload(payload)).digest('hex'); } function isSignatureValid(actual: string, expected: string): boolean { const actualBuffer = Buffer.from(actual, 'hex'); const expectedBuffer = Buffer.from(expected, 'hex'); return ( actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer) ); } function includesId(allowedIds: readonly string[], id: string): boolean { return allowedIds.includes(id); } /** Creates the signed, auditable envelope accepted by the gateway Discord service boundary. */ export function createDiscordIngressEnvelope( payload: DiscordIngressPayload, serviceToken: string, ): DiscordIngressEnvelope { return { payload, signature: signPayload(payload, serviceToken) }; } /** * Verifies service-origin integrity and applies default-deny Discord identity allowlists. * Returns null instead of a partially trusted payload on every failure path. */ export function verifyDiscordIngressEnvelope( envelope: DiscordIngressEnvelope, serviceToken: string, allowlists?: DiscordIngressAllowlists, ): DiscordIngressPayload | null { const expectedSignature = signPayload(envelope.payload, serviceToken); if (!isSignatureValid(envelope.signature, expectedSignature)) return null; if ( allowlists && (!includesId(allowlists.guildIds, envelope.payload.guildId) || !includesId(allowlists.channelIds, envelope.payload.channelId) || !includesId(allowlists.userIds, envelope.payload.userId)) ) { return null; } return envelope.payload; } export class DiscordPlugin implements OfficialChannelAdapter, ChannelEgressPort { readonly name = 'discord'; private client: Client; private socket: Socket | null = null; /** Bounded last-authorized routes for response-target egress validation. */ private conversationRoutes = new Map(); /** Track in-flight responses to avoid duplicate streaming. */ private pendingResponses = new Map(); private readonly messageRateWindows = new Map(); private readonly threadRateWindows = new Map(); constructor( private readonly config: DiscordPluginConfig, private readonly dependencies: DiscordPluginDependencies = {}, ) { this.client = dependencies.client ?? new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages, ], }); } async start(): Promise { const socketFactory = this.dependencies.socketFactory ?? io; this.socket = socketFactory(`${this.config.gatewayUrl}/chat`, { auth: { discordServiceToken: this.config.serviceToken }, transports: ['websocket'], }); this.socket.on('connect', () => { console.log('[discord] Connected to gateway'); }); this.socket.on('disconnect', (reason: string) => { console.error(`[discord] Disconnected from gateway: ${reason}`); this.pendingResponses.clear(); }); this.socket.on('connect_error', (err: Error) => { console.error(`[discord] Gateway connection error: ${err.message}`); }); this.socket.on('agent:text', (data: { conversationId: string; text: string }) => { const pending = this.pendingResponses.get(data.conversationId); if (pending !== undefined) { this.pendingResponses.set(data.conversationId, pending + data.text); } }); this.socket.on('agent:end', (data: { conversationId: string }) => { const text = this.pendingResponses.get(data.conversationId); this.pendingResponses.delete(data.conversationId); if (text) { this.sendAgentResponse(data.conversationId, text).catch((err: unknown) => { console.error(`[discord] Error sending response for ${data.conversationId}:`, err); }); } else { this.conversationRoutes.delete(data.conversationId); } }); this.socket.on('agent:start', (data: { conversationId: string }) => { this.pendingResponses.set(data.conversationId, ''); }); this.socket.on('error', (data: { conversationId?: unknown }) => { if (typeof data.conversationId !== 'string') return; this.pendingResponses.delete(data.conversationId); this.conversationRoutes.delete(data.conversationId); }); this.client.on('messageCreate', (message: DiscordMessage) => { void Promise.resolve(this.handleDiscordMessage(message)).catch((error: unknown): void => { const errorName = error instanceof Error ? error.name : 'UnknownError'; console.error( `[discord] Message routing failed. channel=${message.channelId} message=${message.id} error=${errorName}`, ); }); }); this.client.on('ready', () => { console.log(`[discord] Bot logged in as ${this.client.user?.tag}`); }); try { await this.client.login(this.config.token); } catch (error: unknown) { this.socket.disconnect(); this.socket = null; await this.client.destroy(); throw error; } } async stop(): Promise { this.socket?.disconnect(); await this.client.destroy(); } async health(): Promise { const discordReady = this.client.isReady(); const gatewayConnected = this.socket?.connected === true; if (discordReady && gatewayConnected) return { status: 'connected' }; if (discordReady || gatewayConnected) return { status: 'degraded' }; return { status: 'disconnected' }; } async createProjectChannel(project: { id: string; name: string; description?: string; }): Promise<{ channelId: string } | null> { if (!this.config.guildId) return null; const guild = this.client.guilds.cache.get(this.config.guildId); if (!guild) return null; const channelName = `mosaic-${project.name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '')}`; const channel = await guild.channels.create({ name: channelName, type: ChannelType.GuildText, topic: project.description ?? `Mosaic project: ${project.name}`, }); // A project channel has no logical-agent conversation until a configured // binding authorizes a message. Do not seed a legacy channel-only key. return { channelId: channel.id }; } private handleDiscordMessage(message: DiscordMessage): void | Promise { if (message.author.bot || !this.client.user || !message.guildId) return; const authorizationChannelId = this.authorizationChannelId(message); if (!this.isAllowedMessage(message, authorizationChannelId)) return; const isMention = message.mentions.has(this.client.user); const content = isMention ? message.content.replace(new RegExp(`<@!?${this.client.user.id}>`, 'g'), '').trim() : message.content.trim(); if (!content && message.attachments.size === 0) return; const operation = this.interactionOperation(content); const binding = resolveDiscordInteractionBinding( this.config.interactionBindings ?? [], message.guildId, authorizationChannelId, message.author.id, operation, ); // Pairing and operation-specific role checks happen before thread creation // or any gateway dispatch, including privileged control events. if (!binding) return; const rateKey = `${message.guildId}:${authorizationChannelId}:${message.author.id}`; if ( !this.consumeRateLimit( this.messageRateWindows, rateKey, this.config.messageRateLimitPerMinute ?? DEFAULT_MESSAGE_RATE_LIMIT_PER_MINUTE, ) ) { console.error( `[discord] Message rate limit reached. guild=${message.guildId} channel=${authorizationChannelId} user=${message.author.id}`, ); return; } const createThread = isMention && operation === 'send'; if ( createThread && !this.consumeRateLimit( this.threadRateWindows, rateKey, this.config.threadRateLimitPerMinute ?? DEFAULT_THREAD_RATE_LIMIT_PER_MINUTE, ) ) { console.error( `[discord] Thread rate limit reached. guild=${message.guildId} channel=${authorizationChannelId} user=${message.author.id}`, ); return; } if (!this.dependencies.ingressPort && !this.socket?.connected) { console.error( `[discord] Cannot forward message: not connected to gateway. channel=${message.channelId} message=${message.id}`, ); return; } // Approval/stop commands act on the current durable session. They remain // at the current channel/thread target rather than creating a new topic. const route = this.resolveConversationRoute( message, binding, authorizationChannelId, createThread, ); if (route instanceof Promise) { return route.then((resolved: ChannelConversationRouteDto): void | Promise => this.dispatchDiscordIngress(message, content, operation, binding, resolved), ); } return this.dispatchDiscordIngress(message, content, operation, binding, route); } private dispatchDiscordIngress( message: DiscordMessage, content: string, operation: 'send' | 'approve' | 'stop', binding: DiscordInteractionBinding, route: ChannelConversationRouteDto, ): void | Promise { const guildId = message.guildId; if (!guildId) return; if (operation === 'send') this.rememberConversationRoute(route); const correlationId = randomUUID(); const ingress: ChannelIngressDto = { correlationId, nativeMessageId: message.id, operation: operation === 'approve' ? 'approval.create' : operation === 'stop' ? 'session.stop' : 'message.send', principal: this.authorizedPrincipal(binding, message.author.id), message: this.channelMessage(message, content, route), route, }; if (this.dependencies.ingressPort) { return this.dependencies.ingressPort.receive(ingress).catch((error: unknown) => { if (operation === 'send') this.conversationRoutes.delete(route.conversationId); throw error; }); } const socket = this.socket; if (!socket?.connected) { console.error( `[discord] Cannot dispatch routed message: gateway disconnected. channel=${message.channelId} message=${message.id}`, ); return; } this.emitDiscordIngress(socket, ingress); } private authorizedPrincipal( binding: DiscordInteractionBinding, channelUserId: string, ): ChannelAuthorizedPrincipalDto { const pairing = binding.pairedUsers[channelUserId]; if (!pairing) throw new Error('Authorized Discord pairing is unavailable'); if (typeof pairing === 'string') return { channelUserId, role: pairing }; return { channelUserId, role: pairing.role, ...(pairing.mosaicUserId ? { mosaicUserId: pairing.mosaicUserId } : {}), }; } private channelMessage( message: DiscordMessage, content: string, route: ChannelConversationRouteDto, ): ChannelMessageDto { const attachments = Array.from(message.attachments.values()).map((attachment) => ({ id: attachment.id, name: attachment.name, url: attachment.url, mimeType: attachment.contentType, sizeBytes: attachment.size, })); const firstContentType = attachments[0]?.mimeType; return { id: randomUUID(), channelName: this.name, channelId: route.responseTarget.channelId, senderId: message.author.id, senderKind: 'user', content, contentKind: content.length > 0 ? 'markdown' : firstContentType?.startsWith('image/') ? 'image' : 'file', timestamp: message.createdAt instanceof Date ? message.createdAt.toISOString() : new Date().toISOString(), ...(route.responseTarget.threadId ? { threadId: route.responseTarget.threadId } : {}), ...(attachments.length > 0 ? { attachments } : {}), metadata: { channelMessageId: message.id, guildId: message.guildId ?? '', }, }; } private emitDiscordIngress(socket: Socket, ingress: ChannelIngressDto): void { const envelope = createDiscordIngressEnvelope( { correlationId: ingress.correlationId, messageId: ingress.nativeMessageId, guildId: String(ingress.message.metadata['guildId'] ?? ''), channelId: ingress.route.authorizationChannelId, userId: ingress.principal.channelUserId, conversationId: ingress.route.conversationId, content: ingress.message.content, ...(ingress.route.responseTarget.threadId ? { threadId: ingress.route.responseTarget.threadId } : {}), attachments: ingress.message.attachments?.map((attachment) => ({ id: attachment.id, name: attachment.name, url: attachment.url, contentType: attachment.mimeType, ...(attachment.sizeBytes !== undefined ? { sizeBytes: attachment.sizeBytes } : {}), })), }, this.config.serviceToken, ); socket.emit( ingress.operation === 'approval.create' ? 'discord:approve' : ingress.operation === 'session.stop' ? 'discord:stop' : 'message', envelope, ); } private authorizationChannelId(message: DiscordMessage): string { // A normal guild channel can itself have a category parent. Only Discord // threads inherit authorization from a configured parent text channel. const channel = message.channel as DiscordMessage['channel'] & { isThread?: () => boolean; parentId?: string | null; }; const isThread = typeof channel.isThread === 'function' ? channel.isThread() : channel.parentId !== undefined && channel.parentId !== null; return isThread && channel.parentId ? channel.parentId : message.channelId; } private isAllowedMessage(message: DiscordMessage, authorizationChannelId: string): boolean { const guildId = message.guildId; return ( guildId !== null && includesId(this.config.allowedGuildIds, guildId) && includesId(this.config.allowedChannelIds, authorizationChannelId) && includesId(this.config.allowedUserIds, message.author.id) ); } private resolveConversationRoute( message: DiscordMessage, binding: DiscordInteractionBinding, authorizationChannelId: string, createThread: boolean, ): ChannelConversationRouteDto | Promise { if (authorizationChannelId !== message.channelId) { return this.createConversationRoute( binding, authorizationChannelId, message.channelId, message.channelId, ); } if (!createThread) { return this.createConversationRoute(binding, authorizationChannelId, message.channelId); } if (message.hasThread) { const cachedThread = message.thread; if (cachedThread) { return this.createConversationRoute( binding, authorizationChannelId, cachedThread.id, cachedThread.id, ); } if (!('threads' in message.channel)) { return Promise.reject(new Error('Existing Discord thread manager is unavailable')); } return message.channel.threads .fetch(message.id) .then((thread): ChannelConversationRouteDto => { if (!thread) throw new Error('Existing Discord thread is unavailable'); return this.createConversationRoute( binding, authorizationChannelId, thread.id, thread.id, ); }); } return message .startThread({ name: `Mosaic conversation ${message.id.slice(-8)}`, autoArchiveDuration: ThreadAutoArchiveDuration.OneHour, reason: 'Authorized Mosaic mention', }) .then( (thread): ChannelConversationRouteDto => this.createConversationRoute(binding, authorizationChannelId, thread.id, thread.id), ); } private createConversationRoute( binding: DiscordInteractionBinding, authorizationChannelId: string, responseChannelId: string, threadId?: string, ): ChannelConversationRouteDto { // Recompute from configuration on every turn so a stale in-memory map can // never carry a channel-only or differently bound agent identity forward. const conversationId = `${binding.instanceId}:discord:${responseChannelId}`; return { bindingId: `${binding.guildId}:${binding.channelId}:${binding.instanceId}`, logicalAgentId: binding.instanceId, conversationId, channelName: this.name, authorizationChannelId, responseTarget: { channelId: responseChannelId, ...(threadId ? { threadId } : {}), }, }; } private consumeRateLimit( windows: Map, key: string, limit: number, now = Date.now(), ): boolean { const active = (windows.get(key) ?? []).filter( (timestamp: number): boolean => now - timestamp < RATE_LIMIT_WINDOW_MS, ); if (active.length >= Math.max(1, limit)) { windows.set(key, active); return false; } active.push(now); windows.set(key, active); return true; } private interactionOperation(content: string): 'send' | 'approve' | 'stop' { if (/^\/approve$/i.test(content)) return 'approve'; if (/^\/stop\s+\S+/i.test(content)) return 'stop'; return 'send'; } async send(egress: ChannelEgressDto): Promise { if (!this.isConfiguredRoute(egress.route) || !this.isMessageAlignedWithRoute(egress)) { throw new ChannelDeliveryError( 'invalid_route', `Discord egress route is not authorized for conversation ${egress.route.conversationId}`, ); } const channelId = egress.route.responseTarget.channelId; const channel = this.client.channels.cache.get(channelId); if (!channel || !('send' in channel)) { throw new ChannelDeliveryError( 'destination_unavailable', `Discord destination is unavailable for conversation ${egress.route.conversationId}`, ); } const chunks = this.chunkText(egress.message.content, 1900); for (const [chunkIndex, chunk] of chunks.entries()) { await this.sendChunkWithRetry( channel as { send(options: { content: string; nonce: string; enforceNonce: true }): Promise; }, chunk, egress.correlationId, chunkIndex, egress.route.conversationId, ); } } private async sendChunkWithRetry( channel: { send(options: { content: string; nonce: string; enforceNonce: true }): Promise; }, chunk: string, correlationId: string, chunkIndex: number, conversationId: string, ): Promise { const nonce = createHash('sha256') .update(`${correlationId}:${chunkIndex}`) .digest('hex') .slice(0, 25); let lastError: unknown; for (let attempt = 1; attempt <= DELIVERY_MAX_ATTEMPTS; attempt += 1) { try { await channel.send({ content: chunk, nonce, enforceNonce: true }); return; } catch (error: unknown) { lastError = error; const retryable = this.isTransientDeliveryError(error); if (!retryable) { throw new ChannelDeliveryError( 'delivery_failed', `Discord delivery failed for conversation ${conversationId}`, false, { cause: error }, ); } if (attempt < DELIVERY_MAX_ATTEMPTS) { await new Promise((resolve): void => { setTimeout(resolve, DELIVERY_RETRY_BASE_MS * 2 ** (attempt - 1)); }); } } } throw new ChannelDeliveryError( 'delivery_failed', `Discord delivery failed for conversation ${conversationId}`, true, { cause: lastError }, ); } private isTransientDeliveryError(error: unknown): boolean { if (typeof error !== 'object' || error === null) return false; const candidate = error as { status?: unknown; code?: unknown }; if ( typeof candidate.status === 'number' && (candidate.status === 429 || candidate.status >= 500) ) { return true; } return ( typeof candidate.code === 'string' && ['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'UND_ERR_CONNECT_TIMEOUT'].includes(candidate.code) ); } private async sendAgentResponse(conversationId: string, text: string): Promise { const route = this.conversationRoutes.get(conversationId); if (!route) { throw new ChannelDeliveryError( 'invalid_route', `Discord response route is unavailable for conversation ${conversationId}`, ); } try { await this.send({ correlationId: randomUUID(), route, message: { id: randomUUID(), channelName: this.name, channelId: route.responseTarget.channelId, senderId: route.logicalAgentId, senderKind: 'agent', content: text, contentKind: 'markdown', timestamp: new Date().toISOString(), ...(route.responseTarget.threadId ? { threadId: route.responseTarget.threadId } : {}), metadata: {}, }, }); } finally { this.conversationRoutes.delete(conversationId); } } /** Compatibility wrapper while Socket.IO agent events carry only conversation ID. */ private async sendToDiscord(conversationId: string, text: string): Promise { await this.sendAgentResponse(conversationId, text); } private rememberConversationRoute(route: ChannelConversationRouteDto): void { if ( !this.conversationRoutes.has(route.conversationId) && this.conversationRoutes.size >= MAX_CONVERSATION_ROUTES ) { throw new ChannelDeliveryError( 'delivery_failed', 'Discord has reached its active response-route limit', true, ); } this.conversationRoutes.set(route.conversationId, route); } private isConfiguredRoute(route: ChannelConversationRouteDto): boolean { if ( route.channelName !== this.name || route.conversationId !== `${route.logicalAgentId}:${this.name}:${route.responseTarget.channelId}` ) { return false; } const bindingMatches = (this.config.interactionBindings ?? []).some( (binding): boolean => route.bindingId === `${binding.guildId}:${binding.channelId}:${binding.instanceId}` && route.logicalAgentId === binding.instanceId && route.authorizationChannelId === binding.channelId, ); if (!bindingMatches) return false; if ( route.responseTarget.channelId === route.authorizationChannelId && route.responseTarget.threadId === undefined ) { return true; } const observed = this.conversationRoutes.get(route.conversationId); return ( observed?.bindingId === route.bindingId && observed.logicalAgentId === route.logicalAgentId && observed.authorizationChannelId === route.authorizationChannelId && observed.responseTarget.channelId === route.responseTarget.channelId && observed.responseTarget.threadId === route.responseTarget.threadId ); } private isMessageAlignedWithRoute(egress: ChannelEgressDto): boolean { return ( egress.message.channelName === this.name && egress.message.channelId === egress.route.responseTarget.channelId && egress.message.threadId === egress.route.responseTarget.threadId ); } private chunkText(text: string, maxLength: number): string[] { if (text.length <= maxLength) return [text]; const chunks: string[] = []; let remaining = text; while (remaining.length > 0) { if (remaining.length <= maxLength) { chunks.push(remaining); break; } let breakPoint = remaining.lastIndexOf('\n', maxLength); if (breakPoint <= 0) breakPoint = maxLength; chunks.push(remaining.slice(0, breakPoint)); remaining = remaining.slice(breakPoint).trimStart(); } return chunks; } }