--- title: Outbound Session Mirroring Refactor (Issue #1520) description: Track outbound session mirroring refactor notes, decisions, tests, and open items. --- # Outbound Session Mirroring Refactor (Issue #1520) ## Status - In progress. - Core + plugin channel routing updated for outbound mirroring. - Gateway send now derives target session when sessionKey is omitted. ## Context Outbound sends were mirrored into the *current* agent session (tool session key) rather than the target channel session. Inbound routing uses channel/peer session keys, so outbound responses landed in the wrong session and first-contact targets often lacked session entries. ## Goals - Mirror outbound messages into the target channel session key. - Create session entries on outbound when missing. - Keep thread/topic scoping aligned with inbound session keys. - Cover core channels plus bundled extensions. ## Implementation Summary - New outbound session routing helper: - `src/infra/outbound/outbound-session.ts` - `resolveOutboundSessionRoute` builds target sessionKey using `buildAgentSessionKey` (dmScope + identityLinks). - `ensureOutboundSessionEntry` writes minimal `MsgContext` via `recordSessionMetaFromInbound`. - `runMessageAction` (send) derives target sessionKey and passes it to `executeSendAction` for mirroring. - `message-tool` no longer mirrors directly; it only resolves agentId from the current session key. - Plugin send path mirrors via `appendAssistantMessageToSessionTranscript` using the derived sessionKey. - Gateway send derives a target session key when none is provided (default agent), and ensures a session entry. ## Thread/Topic Handling - Slack: replyTo/threadId -> `resolveThreadSessionKeys` (suffix). - Discord: threadId/replyTo -> `resolveThreadSessionKeys` with `useSuffix=false` to match inbound (thread channel id already scopes session). - Telegram: topic IDs map to `chatId:topic:` via `buildTelegramGroupPeerId`. ## Extensions Covered - Matrix, MS Teams, Mattermost, BlueBubbles, Nextcloud Talk, Zalo, Zalo Personal, Nostr, Tlon. - Notes: - Mattermost targets now strip `@` for DM session key routing. - Zalo Personal uses DM peer kind for 1:1 targets (group only when `group:` is present). - BlueBubbles group targets strip `chat_*` prefixes to match inbound session keys. - Slack auto-thread mirroring matches channel ids case-insensitively. - Gateway send lowercases provided session keys before mirroring. ## Decisions - **Gateway send session derivation**: if `sessionKey` is provided, use it. If omitted, derive a sessionKey from target + default agent and mirror there. - **Session entry creation**: always use `recordSessionMetaFromInbound` with `Provider/From/To/ChatType/AccountId/Originating*` aligned to inbound formats. - **Target normalization**: outbound routing uses resolved targets (post `resolveChannelTarget`) when available. - **Session key casing**: canonicalize session keys to lowercase on write and during migrations. ## Tests Added/Updated - `src/infra/outbound/outbound-session.test.ts` - Slack thread session key. - Telegram topic session key. - dmScope identityLinks with Discord. - `src/agents/tools/message-tool.test.ts` - Derives agentId from session key (no sessionKey passed through). - `src/gateway/server-methods/send.test.ts` - Derives session key when omitted and creates session entry. ## Open Items / Follow-ups - Voice-call plugin uses custom `voice:` session keys. Outbound mapping is not standardized here; if message-tool should support voice-call sends, add explicit mapping. - Confirm if any external plugin uses non-standard `From/To` formats beyond the bundled set. ## Files Touched - `src/infra/outbound/outbound-session.ts` - `src/infra/outbound/outbound-send-service.ts` - `src/infra/outbound/message-action-runner.ts` - `src/agents/tools/message-tool.ts` - `src/gateway/server-methods/send.ts` - Tests in: - `src/infra/outbound/outbound-session.test.ts` - `src/agents/tools/message-tool.test.ts` - `src/gateway/server-methods/send.test.ts`