feat(matrix): Add multi-account support to Matrix channel

The Matrix channel previously hardcoded `listMatrixAccountIds` to always
return only `DEFAULT_ACCOUNT_ID`, ignoring any accounts configured in
`channels.matrix.accounts`. This prevented running multiple Matrix bot
accounts simultaneously.

Changes:
- Update `listMatrixAccountIds` to read from `channels.matrix.accounts`
  config, falling back to `DEFAULT_ACCOUNT_ID` for legacy single-account
  configurations
- Add `resolveMatrixConfigForAccount` to resolve config for a specific
  account ID, merging account-specific values with top-level defaults
- Update `resolveMatrixAccount` to use account-specific config when
  available
- The multi-account config structure (channels.matrix.accounts) was not
  defined in the MatrixConfig type, causing TypeScript to not recognize
  the field. Added the accounts field to properly type the multi-account
  configuration.
- Add stopSharedClientForAccount() to stop only the specific account's
  client instead of all clients when an account shuts down
- Wrap dynamic import in try/finally to prevent startup mutex deadlock
  if the import fails
- Pass accountId to resolveSharedMatrixClient(), resolveMatrixAuth(),
  and createMatrixClient() to ensure the correct account's credentials
  are used for outbound messages
- Add accountId parameter to resolveMediaMaxBytes to check account-specific
  config before falling back to top-level config
- Maintain backward compatibility with existing single-account setups

This follows the same pattern already used by the WhatsApp channel for
multi-account support.

Fixes #3165
Fixes #3085

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Monty Taylor
2026-02-02 07:28:39 -08:00
committed by Peter Steinberger
parent 607b625aab
commit caf5d2dd7c
17 changed files with 367 additions and 103 deletions

View File

@@ -7,13 +7,14 @@ export const matrixOutbound: ChannelOutboundAdapter = {
chunker: (text, limit) => getMatrixRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
textChunkLimit: 4000,
sendText: async ({ to, text, deps, replyToId, threadId }) => {
sendText: async ({ to, text, deps, replyToId, threadId, accountId }) => {
const send = deps?.sendMatrix ?? sendMessageMatrix;
const resolvedThreadId =
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
const result = await send(to, text, {
replyToId: replyToId ?? undefined,
threadId: resolvedThreadId,
accountId: accountId ?? undefined,
});
return {
channel: "matrix",
@@ -21,7 +22,7 @@ export const matrixOutbound: ChannelOutboundAdapter = {
roomId: result.roomId,
};
},
sendMedia: async ({ to, text, mediaUrl, deps, replyToId, threadId }) => {
sendMedia: async ({ to, text, mediaUrl, deps, replyToId, threadId, accountId }) => {
const send = deps?.sendMatrix ?? sendMessageMatrix;
const resolvedThreadId =
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
@@ -29,6 +30,7 @@ export const matrixOutbound: ChannelOutboundAdapter = {
mediaUrl,
replyToId: replyToId ?? undefined,
threadId: resolvedThreadId,
accountId: accountId ?? undefined,
});
return {
channel: "matrix",
@@ -36,11 +38,12 @@ export const matrixOutbound: ChannelOutboundAdapter = {
roomId: result.roomId,
};
},
sendPoll: async ({ to, poll, threadId }) => {
sendPoll: async ({ to, poll, threadId, accountId }) => {
const resolvedThreadId =
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
const result = await sendPollMatrix(to, poll, {
threadId: resolvedThreadId,
accountId: accountId ?? undefined,
});
return {
channel: "matrix",