---
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_ koi 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 `buildKoiSessionKey` (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 koiId 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 koi), 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:<id>` 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 koi 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/koi/tools/message-tool.test.ts`
  - Derives koiId 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/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/koi/tools/message-tool.ts`
- `src/gateway/server-methods/send.ts`
- Tests in:
  - `src/infra/outbound/outbound-session.test.ts`
  - `src/koi/tools/message-tool.test.ts`
  - `src/gateway/server-methods/send.test.ts`
