@openclaw/matrix 2026.2.12 → 2026.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
3
  import type { CoreConfig } from "../../types.js";
3
4
  import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
4
5
  import { getMatrixRuntime } from "../../runtime.js";
@@ -8,11 +9,49 @@ function clean(value?: string): string {
8
9
  return value?.trim() ?? "";
9
10
  }
10
11
 
11
- export function resolveMatrixConfig(
12
+ /** Shallow-merge known nested config sub-objects so partial overrides inherit base values. */
13
+ function deepMergeConfig<T extends Record<string, unknown>>(base: T, override: Partial<T>): T {
14
+ const merged = { ...base, ...override } as Record<string, unknown>;
15
+ // Merge known nested objects (dm, actions) so partial overrides keep base fields
16
+ for (const key of ["dm", "actions"] as const) {
17
+ const b = base[key];
18
+ const o = override[key];
19
+ if (typeof b === "object" && b !== null && typeof o === "object" && o !== null) {
20
+ merged[key] = { ...(b as Record<string, unknown>), ...(o as Record<string, unknown>) };
21
+ }
22
+ }
23
+ return merged as T;
24
+ }
25
+
26
+ /**
27
+ * Resolve Matrix config for a specific account, with fallback to top-level config.
28
+ * This supports both multi-account (channels.matrix.accounts.*) and
29
+ * single-account (channels.matrix.*) configurations.
30
+ */
31
+ export function resolveMatrixConfigForAccount(
12
32
  cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
33
+ accountId?: string | null,
13
34
  env: NodeJS.ProcessEnv = process.env,
14
35
  ): MatrixResolvedConfig {
15
- const matrix = cfg.channels?.matrix ?? {};
36
+ const normalizedAccountId = normalizeAccountId(accountId);
37
+ const matrixBase = cfg.channels?.matrix ?? {};
38
+ const accounts = cfg.channels?.matrix?.accounts;
39
+
40
+ // Try to get account-specific config first (direct lookup, then case-insensitive fallback)
41
+ let accountConfig = accounts?.[normalizedAccountId];
42
+ if (!accountConfig && accounts) {
43
+ for (const key of Object.keys(accounts)) {
44
+ if (normalizeAccountId(key) === normalizedAccountId) {
45
+ accountConfig = accounts[key];
46
+ break;
47
+ }
48
+ }
49
+ }
50
+
51
+ // Deep merge: account-specific values override top-level values, preserving
52
+ // nested object inheritance (dm, actions, groups) so partial overrides work.
53
+ const matrix = accountConfig ? deepMergeConfig(matrixBase, accountConfig) : matrixBase;
54
+
16
55
  const homeserver = clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER);
17
56
  const userId = clean(matrix.userId) || clean(env.MATRIX_USER_ID);
18
57
  const accessToken = clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
@@ -34,13 +73,24 @@ export function resolveMatrixConfig(
34
73
  };
35
74
  }
36
75
 
76
+ /**
77
+ * Single-account function for backward compatibility - resolves default account config.
78
+ */
79
+ export function resolveMatrixConfig(
80
+ cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
81
+ env: NodeJS.ProcessEnv = process.env,
82
+ ): MatrixResolvedConfig {
83
+ return resolveMatrixConfigForAccount(cfg, DEFAULT_ACCOUNT_ID, env);
84
+ }
85
+
37
86
  export async function resolveMatrixAuth(params?: {
38
87
  cfg?: CoreConfig;
39
88
  env?: NodeJS.ProcessEnv;
89
+ accountId?: string | null;
40
90
  }): Promise<MatrixAuth> {
41
91
  const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig);
42
92
  const env = params?.env ?? process.env;
43
- const resolved = resolveMatrixConfig(cfg, env);
93
+ const resolved = resolveMatrixConfigForAccount(cfg, params?.accountId, env);
44
94
  if (!resolved.homeserver) {
45
95
  throw new Error("Matrix homeserver is required (matrix.homeserver)");
46
96
  }
@@ -52,7 +102,8 @@ export async function resolveMatrixAuth(params?: {
52
102
  touchMatrixCredentials,
53
103
  } = await import("../credentials.js");
54
104
 
55
- const cached = loadMatrixCredentials(env);
105
+ const accountId = params?.accountId;
106
+ const cached = loadMatrixCredentials(env, accountId);
56
107
  const cachedCredentials =
57
108
  cached &&
58
109
  credentialsMatchConfig(cached, {
@@ -72,13 +123,17 @@ export async function resolveMatrixAuth(params?: {
72
123
  const whoami = await tempClient.getUserId();
73
124
  userId = whoami;
74
125
  // Save the credentials with the fetched userId
75
- saveMatrixCredentials({
76
- homeserver: resolved.homeserver,
77
- userId,
78
- accessToken: resolved.accessToken,
79
- });
126
+ saveMatrixCredentials(
127
+ {
128
+ homeserver: resolved.homeserver,
129
+ userId,
130
+ accessToken: resolved.accessToken,
131
+ },
132
+ env,
133
+ accountId,
134
+ );
80
135
  } else if (cachedCredentials && cachedCredentials.accessToken === resolved.accessToken) {
81
- touchMatrixCredentials(env);
136
+ touchMatrixCredentials(env, accountId);
82
137
  }
83
138
  return {
84
139
  homeserver: resolved.homeserver,
@@ -91,7 +146,7 @@ export async function resolveMatrixAuth(params?: {
91
146
  }
92
147
 
93
148
  if (cachedCredentials) {
94
- touchMatrixCredentials(env);
149
+ touchMatrixCredentials(env, accountId);
95
150
  return {
96
151
  homeserver: cachedCredentials.homeserver,
97
152
  userId: cachedCredentials.userId,
@@ -149,12 +204,16 @@ export async function resolveMatrixAuth(params?: {
149
204
  encryption: resolved.encryption,
150
205
  };
151
206
 
152
- saveMatrixCredentials({
153
- homeserver: auth.homeserver,
154
- userId: auth.userId,
155
- accessToken: auth.accessToken,
156
- deviceId: login.device_id,
157
- });
207
+ saveMatrixCredentials(
208
+ {
209
+ homeserver: auth.homeserver,
210
+ userId: auth.userId,
211
+ accessToken: auth.accessToken,
212
+ deviceId: login.device_id,
213
+ },
214
+ env,
215
+ accountId,
216
+ );
158
217
 
159
218
  return auth;
160
219
  }
@@ -1,5 +1,6 @@
1
1
  import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
2
  import { LogService } from "@vector-im/matrix-bot-sdk";
3
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
4
  import type { CoreConfig } from "../../types.js";
4
5
  import type { MatrixAuth } from "./types.js";
5
6
  import { resolveMatrixAuth } from "./config.js";
@@ -13,17 +14,19 @@ type SharedMatrixClientState = {
13
14
  cryptoReady: boolean;
14
15
  };
15
16
 
16
- let sharedClientState: SharedMatrixClientState | null = null;
17
- let sharedClientPromise: Promise<SharedMatrixClientState> | null = null;
18
- let sharedClientStartPromise: Promise<void> | null = null;
17
+ // Support multiple accounts with separate clients
18
+ const sharedClientStates = new Map<string, SharedMatrixClientState>();
19
+ const sharedClientPromises = new Map<string, Promise<SharedMatrixClientState>>();
20
+ const sharedClientStartPromises = new Map<string, Promise<void>>();
19
21
 
20
22
  function buildSharedClientKey(auth: MatrixAuth, accountId?: string | null): string {
23
+ const normalizedAccountId = normalizeAccountId(accountId);
21
24
  return [
22
25
  auth.homeserver,
23
26
  auth.userId,
24
27
  auth.accessToken,
25
28
  auth.encryption ? "e2ee" : "plain",
26
- accountId ?? DEFAULT_ACCOUNT_KEY,
29
+ normalizedAccountId || DEFAULT_ACCOUNT_KEY,
27
30
  ].join("|");
28
31
  }
29
32
 
@@ -57,11 +60,13 @@ async function ensureSharedClientStarted(params: {
57
60
  if (params.state.started) {
58
61
  return;
59
62
  }
60
- if (sharedClientStartPromise) {
61
- await sharedClientStartPromise;
63
+ const key = params.state.key;
64
+ const existingStartPromise = sharedClientStartPromises.get(key);
65
+ if (existingStartPromise) {
66
+ await existingStartPromise;
62
67
  return;
63
68
  }
64
- sharedClientStartPromise = (async () => {
69
+ const startPromise = (async () => {
65
70
  const client = params.state.client;
66
71
 
67
72
  // Initialize crypto if enabled
@@ -82,10 +87,11 @@ async function ensureSharedClientStarted(params: {
82
87
  await client.start();
83
88
  params.state.started = true;
84
89
  })();
90
+ sharedClientStartPromises.set(key, startPromise);
85
91
  try {
86
- await sharedClientStartPromise;
92
+ await startPromise;
87
93
  } finally {
88
- sharedClientStartPromise = null;
94
+ sharedClientStartPromises.delete(key);
89
95
  }
90
96
  }
91
97
 
@@ -99,48 +105,51 @@ export async function resolveSharedMatrixClient(
99
105
  accountId?: string | null;
100
106
  } = {},
101
107
  ): Promise<MatrixClient> {
102
- const auth = params.auth ?? (await resolveMatrixAuth({ cfg: params.cfg, env: params.env }));
103
- const key = buildSharedClientKey(auth, params.accountId);
108
+ const accountId = normalizeAccountId(params.accountId);
109
+ const auth =
110
+ params.auth ?? (await resolveMatrixAuth({ cfg: params.cfg, env: params.env, accountId }));
111
+ const key = buildSharedClientKey(auth, accountId);
104
112
  const shouldStart = params.startClient !== false;
105
113
 
106
- if (sharedClientState?.key === key) {
114
+ // Check if we already have a client for this key
115
+ const existingState = sharedClientStates.get(key);
116
+ if (existingState) {
107
117
  if (shouldStart) {
108
118
  await ensureSharedClientStarted({
109
- state: sharedClientState,
119
+ state: existingState,
110
120
  timeoutMs: params.timeoutMs,
111
121
  initialSyncLimit: auth.initialSyncLimit,
112
122
  encryption: auth.encryption,
113
123
  });
114
124
  }
115
- return sharedClientState.client;
125
+ return existingState.client;
116
126
  }
117
127
 
118
- if (sharedClientPromise) {
119
- const pending = await sharedClientPromise;
120
- if (pending.key === key) {
121
- if (shouldStart) {
122
- await ensureSharedClientStarted({
123
- state: pending,
124
- timeoutMs: params.timeoutMs,
125
- initialSyncLimit: auth.initialSyncLimit,
126
- encryption: auth.encryption,
127
- });
128
- }
129
- return pending.client;
128
+ // Check if there's a pending creation for this key
129
+ const existingPromise = sharedClientPromises.get(key);
130
+ if (existingPromise) {
131
+ const pending = await existingPromise;
132
+ if (shouldStart) {
133
+ await ensureSharedClientStarted({
134
+ state: pending,
135
+ timeoutMs: params.timeoutMs,
136
+ initialSyncLimit: auth.initialSyncLimit,
137
+ encryption: auth.encryption,
138
+ });
130
139
  }
131
- pending.client.stop();
132
- sharedClientState = null;
133
- sharedClientPromise = null;
140
+ return pending.client;
134
141
  }
135
142
 
136
- sharedClientPromise = createSharedMatrixClient({
143
+ // Create a new client for this account
144
+ const createPromise = createSharedMatrixClient({
137
145
  auth,
138
146
  timeoutMs: params.timeoutMs,
139
- accountId: params.accountId,
147
+ accountId,
140
148
  });
149
+ sharedClientPromises.set(key, createPromise);
141
150
  try {
142
- const created = await sharedClientPromise;
143
- sharedClientState = created;
151
+ const created = await createPromise;
152
+ sharedClientStates.set(key, created);
144
153
  if (shouldStart) {
145
154
  await ensureSharedClientStarted({
146
155
  state: created,
@@ -151,7 +160,7 @@ export async function resolveSharedMatrixClient(
151
160
  }
152
161
  return created.client;
153
162
  } finally {
154
- sharedClientPromise = null;
163
+ sharedClientPromises.delete(key);
155
164
  }
156
165
  }
157
166
 
@@ -164,9 +173,29 @@ export async function waitForMatrixSync(_params: {
164
173
  // This is kept for API compatibility but is essentially a no-op now
165
174
  }
166
175
 
167
- export function stopSharedClient(): void {
168
- if (sharedClientState) {
169
- sharedClientState.client.stop();
170
- sharedClientState = null;
176
+ export function stopSharedClient(key?: string): void {
177
+ if (key) {
178
+ // Stop a specific client
179
+ const state = sharedClientStates.get(key);
180
+ if (state) {
181
+ state.client.stop();
182
+ sharedClientStates.delete(key);
183
+ }
184
+ } else {
185
+ // Stop all clients (backward compatible behavior)
186
+ for (const state of sharedClientStates.values()) {
187
+ state.client.stop();
188
+ }
189
+ sharedClientStates.clear();
171
190
  }
172
191
  }
192
+
193
+ /**
194
+ * Stop the shared client for a specific account.
195
+ * Use this instead of stopSharedClient() when shutting down a single account
196
+ * to avoid stopping all accounts.
197
+ */
198
+ export function stopSharedClientForAccount(auth: MatrixAuth, accountId?: string | null): void {
199
+ const key = buildSharedClientKey(auth, normalizeAccountId(accountId));
200
+ stopSharedClient(key);
201
+ }
@@ -1,5 +1,14 @@
1
1
  export type { MatrixAuth, MatrixResolvedConfig } from "./client/types.js";
2
2
  export { isBunRuntime } from "./client/runtime.js";
3
- export { resolveMatrixConfig, resolveMatrixAuth } from "./client/config.js";
3
+ export {
4
+ resolveMatrixConfig,
5
+ resolveMatrixConfigForAccount,
6
+ resolveMatrixAuth,
7
+ } from "./client/config.js";
4
8
  export { createMatrixClient } from "./client/create-client.js";
5
- export { resolveSharedMatrixClient, waitForMatrixSync, stopSharedClient } from "./client/shared.js";
9
+ export {
10
+ resolveSharedMatrixClient,
11
+ waitForMatrixSync,
12
+ stopSharedClient,
13
+ stopSharedClientForAccount,
14
+ } from "./client/shared.js";
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
4
5
  import { getMatrixRuntime } from "../runtime.js";
5
6
 
6
7
  export type MatrixStoredCredentials = {
@@ -12,7 +13,15 @@ export type MatrixStoredCredentials = {
12
13
  lastUsedAt?: string;
13
14
  };
14
15
 
15
- const CREDENTIALS_FILENAME = "credentials.json";
16
+ function credentialsFilename(accountId?: string | null): string {
17
+ const normalized = normalizeAccountId(accountId);
18
+ if (normalized === DEFAULT_ACCOUNT_ID) {
19
+ return "credentials.json";
20
+ }
21
+ // normalizeAccountId produces lowercase [a-z0-9-] strings, already filesystem-safe.
22
+ // Different raw IDs that normalize to the same value are the same logical account.
23
+ return `credentials-${normalized}.json`;
24
+ }
16
25
 
17
26
  export function resolveMatrixCredentialsDir(
18
27
  env: NodeJS.ProcessEnv = process.env,
@@ -22,15 +31,19 @@ export function resolveMatrixCredentialsDir(
22
31
  return path.join(resolvedStateDir, "credentials", "matrix");
23
32
  }
24
33
 
25
- export function resolveMatrixCredentialsPath(env: NodeJS.ProcessEnv = process.env): string {
34
+ export function resolveMatrixCredentialsPath(
35
+ env: NodeJS.ProcessEnv = process.env,
36
+ accountId?: string | null,
37
+ ): string {
26
38
  const dir = resolveMatrixCredentialsDir(env);
27
- return path.join(dir, CREDENTIALS_FILENAME);
39
+ return path.join(dir, credentialsFilename(accountId));
28
40
  }
29
41
 
30
42
  export function loadMatrixCredentials(
31
43
  env: NodeJS.ProcessEnv = process.env,
44
+ accountId?: string | null,
32
45
  ): MatrixStoredCredentials | null {
33
- const credPath = resolveMatrixCredentialsPath(env);
46
+ const credPath = resolveMatrixCredentialsPath(env, accountId);
34
47
  try {
35
48
  if (!fs.existsSync(credPath)) {
36
49
  return null;
@@ -53,13 +66,14 @@ export function loadMatrixCredentials(
53
66
  export function saveMatrixCredentials(
54
67
  credentials: Omit<MatrixStoredCredentials, "createdAt" | "lastUsedAt">,
55
68
  env: NodeJS.ProcessEnv = process.env,
69
+ accountId?: string | null,
56
70
  ): void {
57
71
  const dir = resolveMatrixCredentialsDir(env);
58
72
  fs.mkdirSync(dir, { recursive: true });
59
73
 
60
- const credPath = resolveMatrixCredentialsPath(env);
74
+ const credPath = resolveMatrixCredentialsPath(env, accountId);
61
75
 
62
- const existing = loadMatrixCredentials(env);
76
+ const existing = loadMatrixCredentials(env, accountId);
63
77
  const now = new Date().toISOString();
64
78
 
65
79
  const toSave: MatrixStoredCredentials = {
@@ -71,19 +85,25 @@ export function saveMatrixCredentials(
71
85
  fs.writeFileSync(credPath, JSON.stringify(toSave, null, 2), "utf-8");
72
86
  }
73
87
 
74
- export function touchMatrixCredentials(env: NodeJS.ProcessEnv = process.env): void {
75
- const existing = loadMatrixCredentials(env);
88
+ export function touchMatrixCredentials(
89
+ env: NodeJS.ProcessEnv = process.env,
90
+ accountId?: string | null,
91
+ ): void {
92
+ const existing = loadMatrixCredentials(env, accountId);
76
93
  if (!existing) {
77
94
  return;
78
95
  }
79
96
 
80
97
  existing.lastUsedAt = new Date().toISOString();
81
- const credPath = resolveMatrixCredentialsPath(env);
98
+ const credPath = resolveMatrixCredentialsPath(env, accountId);
82
99
  fs.writeFileSync(credPath, JSON.stringify(existing, null, 2), "utf-8");
83
100
  }
84
101
 
85
- export function clearMatrixCredentials(env: NodeJS.ProcessEnv = process.env): void {
86
- const credPath = resolveMatrixCredentialsPath(env);
102
+ export function clearMatrixCredentials(
103
+ env: NodeJS.ProcessEnv = process.env,
104
+ accountId?: string | null,
105
+ ): void {
106
+ const credPath = resolveMatrixCredentialsPath(env, accountId);
87
107
  try {
88
108
  if (fs.existsSync(credPath)) {
89
109
  fs.unlinkSync(credPath);
@@ -68,6 +68,7 @@ export type MatrixMonitorHandlerParams = {
68
68
  roomId: string,
69
69
  ) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
70
70
  getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
71
+ accountId?: string | null;
71
72
  };
72
73
 
73
74
  export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
@@ -93,6 +94,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
93
94
  directTracker,
94
95
  getRoomInfo,
95
96
  getMemberDisplayName,
97
+ accountId,
96
98
  } = params;
97
99
 
98
100
  return async (roomId: string, event: MatrixRawEvent) => {
@@ -435,6 +437,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
435
437
  const baseRoute = core.channel.routing.resolveAgentRoute({
436
438
  cfg,
437
439
  channel: "matrix",
440
+ accountId,
438
441
  peer: {
439
442
  kind: isDirectMessage ? "direct" : "channel",
440
443
  id: isDirectMessage ? senderId : roomId,
@@ -3,12 +3,13 @@ import { mergeAllowlist, summarizeMapping, type RuntimeEnv } from "openclaw/plug
3
3
  import type { CoreConfig, ReplyToMode } from "../../types.js";
4
4
  import { resolveMatrixTargets } from "../../resolve-targets.js";
5
5
  import { getMatrixRuntime } from "../../runtime.js";
6
+ import { resolveMatrixAccount } from "../accounts.js";
6
7
  import { setActiveMatrixClient } from "../active-client.js";
7
8
  import {
8
9
  isBunRuntime,
9
10
  resolveMatrixAuth,
10
11
  resolveSharedMatrixClient,
11
- stopSharedClient,
12
+ stopSharedClientForAccount,
12
13
  } from "../client.js";
13
14
  import { normalizeMatrixUserId } from "./allowlist.js";
14
15
  import { registerMatrixAutoJoin } from "./auto-join.js";
@@ -121,10 +122,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
121
122
  return allowList.map(String);
122
123
  };
123
124
 
124
- const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
125
- let allowFrom: string[] = (cfg.channels?.matrix?.dm?.allowFrom ?? []).map(String);
126
- let groupAllowFrom: string[] = (cfg.channels?.matrix?.groupAllowFrom ?? []).map(String);
127
- let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms;
125
+ // Resolve account-specific config for multi-account support
126
+ const account = resolveMatrixAccount({ cfg, accountId: opts.accountId });
127
+ const accountConfig = account.config;
128
+
129
+ const allowlistOnly = accountConfig.allowlistOnly === true;
130
+ let allowFrom: string[] = (accountConfig.dm?.allowFrom ?? []).map(String);
131
+ let groupAllowFrom: string[] = (accountConfig.groupAllowFrom ?? []).map(String);
132
+ let roomsConfig = accountConfig.groups ?? accountConfig.rooms;
128
133
 
129
134
  allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom);
130
135
  groupAllowFrom = await resolveUserAllowlist("matrix group allowlist", groupAllowFrom);
@@ -213,13 +218,13 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
213
218
  ...cfg.channels?.matrix?.dm,
214
219
  allowFrom,
215
220
  },
216
- ...(groupAllowFrom.length > 0 ? { groupAllowFrom } : {}),
221
+ groupAllowFrom,
217
222
  ...(roomsConfig ? { groups: roomsConfig } : {}),
218
223
  },
219
224
  },
220
225
  };
221
226
 
222
- const auth = await resolveMatrixAuth({ cfg });
227
+ const auth = await resolveMatrixAuth({ cfg, accountId: opts.accountId });
223
228
  const resolvedInitialSyncLimit =
224
229
  typeof opts.initialSyncLimit === "number"
225
230
  ? Math.max(0, Math.floor(opts.initialSyncLimit))
@@ -234,20 +239,20 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
234
239
  startClient: false,
235
240
  accountId: opts.accountId,
236
241
  });
237
- setActiveMatrixClient(client);
242
+ setActiveMatrixClient(client, opts.accountId);
238
243
 
239
244
  const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
240
245
  const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
241
- const groupPolicyRaw = cfg.channels?.matrix?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
246
+ const groupPolicyRaw = accountConfig.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
242
247
  const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw;
243
- const replyToMode = opts.replyToMode ?? cfg.channels?.matrix?.replyToMode ?? "off";
244
- const threadReplies = cfg.channels?.matrix?.threadReplies ?? "inbound";
245
- const dmConfig = cfg.channels?.matrix?.dm;
248
+ const replyToMode = opts.replyToMode ?? accountConfig.replyToMode ?? "off";
249
+ const threadReplies = accountConfig.threadReplies ?? "inbound";
250
+ const dmConfig = accountConfig.dm;
246
251
  const dmEnabled = dmConfig?.enabled ?? true;
247
252
  const dmPolicyRaw = dmConfig?.policy ?? "pairing";
248
253
  const dmPolicy = allowlistOnly && dmPolicyRaw !== "disabled" ? "allowlist" : dmPolicyRaw;
249
254
  const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "matrix");
250
- const mediaMaxMb = opts.mediaMaxMb ?? cfg.channels?.matrix?.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
255
+ const mediaMaxMb = opts.mediaMaxMb ?? accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
251
256
  const mediaMaxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
252
257
  const startupMs = Date.now();
253
258
  const startupGraceMs = 0;
@@ -279,6 +284,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
279
284
  directTracker,
280
285
  getRoomInfo,
281
286
  getMemberDisplayName,
287
+ accountId: opts.accountId,
282
288
  });
283
289
 
284
290
  registerMatrixMonitorEvents({
@@ -324,9 +330,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
324
330
  const onAbort = () => {
325
331
  try {
326
332
  logVerboseMessage("matrix: stopping client");
327
- stopSharedClient();
333
+ stopSharedClientForAccount(auth, opts.accountId);
328
334
  } finally {
329
- setActiveMatrixClient(null);
335
+ setActiveMatrixClient(null, opts.accountId);
330
336
  resolve();
331
337
  }
332
338
  };
@@ -1,7 +1,8 @@
1
1
  import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
3
  import type { CoreConfig } from "../../types.js";
3
4
  import { getMatrixRuntime } from "../../runtime.js";
4
- import { getActiveMatrixClient } from "../active-client.js";
5
+ import { getActiveMatrixClient, getAnyActiveMatrixClient } from "../active-client.js";
5
6
  import {
6
7
  createMatrixClient,
7
8
  isBunRuntime,
@@ -17,8 +18,35 @@ export function ensureNodeRuntime() {
17
18
  }
18
19
  }
19
20
 
20
- export function resolveMediaMaxBytes(): number | undefined {
21
+ /** Look up account config with case-insensitive key fallback. */
22
+ function findAccountConfig(
23
+ accounts: Record<string, unknown> | undefined,
24
+ accountId: string,
25
+ ): Record<string, unknown> | undefined {
26
+ if (!accounts) return undefined;
27
+ const normalized = normalizeAccountId(accountId);
28
+ // Direct lookup first
29
+ if (accounts[normalized]) return accounts[normalized] as Record<string, unknown>;
30
+ // Case-insensitive fallback
31
+ for (const key of Object.keys(accounts)) {
32
+ if (normalizeAccountId(key) === normalized) {
33
+ return accounts[key] as Record<string, unknown>;
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ export function resolveMediaMaxBytes(accountId?: string): number | undefined {
21
40
  const cfg = getCore().config.loadConfig() as CoreConfig;
41
+ // Check account-specific config first (case-insensitive key matching)
42
+ const accountConfig = findAccountConfig(
43
+ cfg.channels?.matrix?.accounts as Record<string, unknown> | undefined,
44
+ accountId ?? "",
45
+ );
46
+ if (typeof accountConfig?.mediaMaxMb === "number") {
47
+ return (accountConfig.mediaMaxMb as number) * 1024 * 1024;
48
+ }
49
+ // Fall back to top-level config
22
50
  if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") {
23
51
  return cfg.channels.matrix.mediaMaxMb * 1024 * 1024;
24
52
  }
@@ -28,29 +56,49 @@ export function resolveMediaMaxBytes(): number | undefined {
28
56
  export async function resolveMatrixClient(opts: {
29
57
  client?: MatrixClient;
30
58
  timeoutMs?: number;
59
+ accountId?: string;
31
60
  }): Promise<{ client: MatrixClient; stopOnDone: boolean }> {
32
61
  ensureNodeRuntime();
33
62
  if (opts.client) {
34
63
  return { client: opts.client, stopOnDone: false };
35
64
  }
36
- const active = getActiveMatrixClient();
65
+ const accountId =
66
+ typeof opts.accountId === "string" && opts.accountId.trim().length > 0
67
+ ? normalizeAccountId(opts.accountId)
68
+ : undefined;
69
+ // Try to get the client for the specific account
70
+ const active = getActiveMatrixClient(accountId);
37
71
  if (active) {
38
72
  return { client: active, stopOnDone: false };
39
73
  }
74
+ // When no account is specified, try the default account first; only fall back to
75
+ // any active client as a last resort (prevents sending from an arbitrary account).
76
+ if (!accountId) {
77
+ const defaultClient = getActiveMatrixClient(DEFAULT_ACCOUNT_ID);
78
+ if (defaultClient) {
79
+ return { client: defaultClient, stopOnDone: false };
80
+ }
81
+ const anyActive = getAnyActiveMatrixClient();
82
+ if (anyActive) {
83
+ return { client: anyActive, stopOnDone: false };
84
+ }
85
+ }
40
86
  const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT);
41
87
  if (shouldShareClient) {
42
88
  const client = await resolveSharedMatrixClient({
43
89
  timeoutMs: opts.timeoutMs,
90
+ accountId,
44
91
  });
45
92
  return { client, stopOnDone: false };
46
93
  }
47
- const auth = await resolveMatrixAuth();
94
+ const auth = await resolveMatrixAuth({ accountId });
48
95
  const client = await createMatrixClient({
49
96
  homeserver: auth.homeserver,
50
97
  userId: auth.userId,
51
98
  accessToken: auth.accessToken,
52
99
  encryption: auth.encryption,
53
100
  localTimeoutMs: opts.timeoutMs,
101
+ accountId,
54
102
  });
55
103
  if (auth.encryption && client.crypto) {
56
104
  try {