# 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.tsresolveOutboundSessionRoutebuilds target sessionKey usingbuildAgentSessionKey(dmScope + identityLinks).ensureOutboundSessionEntrywrites minimalMsgContextviarecordSessionMetaFromInbound.
runMessageAction(send) derives target sessionKey and passes it toexecuteSendActionfor mirroring.message-toolno longer mirrors directly; it only resolves agentId from the current session key.- Plugin send path mirrors via
appendAssistantMessageToSessionTranscriptusing 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 ->
resolveThreadSessionKeyswithuseSuffix=falseto match inbound (thread channel id already scopes session). - Telegram: topic IDs map to
chatId:topic:<id>viabuildTelegramGroupPeerId.
# 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.
- Mattermost targets now strip
# Decisions
- Gateway send session derivation: if
sessionKeyis provided, use it. If omitted, derive a sessionKey from target + default agent and mirror there. - Session entry creation: always use
recordSessionMetaFromInboundwithProvider/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:<phone>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/Toformats beyond the bundled set.
# Files Touched
src/infra/outbound/outbound-session.tssrc/infra/outbound/outbound-send-service.tssrc/infra/outbound/message-action-runner.tssrc/agents/tools/message-tool.tssrc/gateway/server-methods/send.ts- Tests in:
src/infra/outbound/outbound-session.test.tssrc/agents/tools/message-tool.test.tssrc/gateway/server-methods/send.test.ts