@openclaw/matrix 2026.2.12 → 2026.2.13

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.2.13
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
3
9
  ## 2026.2.6-3
4
10
 
5
11
  ### Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/matrix",
3
- "version": "2026.2.12",
3
+ "version": "2026.2.13",
4
4
  "description": "OpenClaw Matrix channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,9 +1,28 @@
1
1
  import type { PluginRuntime } from "openclaw/plugin-sdk";
2
- import { beforeEach, describe, expect, it } from "vitest";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import type { CoreConfig } from "./types.js";
4
4
  import { matrixPlugin } from "./channel.js";
5
5
  import { setMatrixRuntime } from "./runtime.js";
6
6
 
7
+ vi.mock("@vector-im/matrix-bot-sdk", () => ({
8
+ ConsoleLogger: class {
9
+ trace = vi.fn();
10
+ debug = vi.fn();
11
+ info = vi.fn();
12
+ warn = vi.fn();
13
+ error = vi.fn();
14
+ },
15
+ MatrixClient: class {},
16
+ LogService: {
17
+ setLogger: vi.fn(),
18
+ warn: vi.fn(),
19
+ info: vi.fn(),
20
+ debug: vi.fn(),
21
+ },
22
+ SimpleFsStorageProvider: class {},
23
+ RustSdkCryptoStorageProvider: class {},
24
+ }));
25
+
7
26
  describe("matrix directory", () => {
8
27
  beforeEach(() => {
9
28
  setMatrixRuntime({
@@ -61,4 +80,65 @@ describe("matrix directory", () => {
61
80
  ]),
62
81
  );
63
82
  });
83
+
84
+ it("resolves replyToMode from account config", () => {
85
+ const cfg = {
86
+ channels: {
87
+ matrix: {
88
+ replyToMode: "off",
89
+ accounts: {
90
+ Assistant: {
91
+ replyToMode: "all",
92
+ },
93
+ },
94
+ },
95
+ },
96
+ } as unknown as CoreConfig;
97
+
98
+ expect(matrixPlugin.threading?.resolveReplyToMode).toBeTruthy();
99
+ expect(
100
+ matrixPlugin.threading?.resolveReplyToMode?.({
101
+ cfg,
102
+ accountId: "assistant",
103
+ chatType: "direct",
104
+ }),
105
+ ).toBe("all");
106
+ expect(
107
+ matrixPlugin.threading?.resolveReplyToMode?.({
108
+ cfg,
109
+ accountId: "default",
110
+ chatType: "direct",
111
+ }),
112
+ ).toBe("off");
113
+ });
114
+
115
+ it("resolves group mention policy from account config", () => {
116
+ const cfg = {
117
+ channels: {
118
+ matrix: {
119
+ groups: {
120
+ "!room:example.org": { requireMention: true },
121
+ },
122
+ accounts: {
123
+ Assistant: {
124
+ groups: {
125
+ "!room:example.org": { requireMention: false },
126
+ },
127
+ },
128
+ },
129
+ },
130
+ },
131
+ } as unknown as CoreConfig;
132
+
133
+ expect(matrixPlugin.groups.resolveRequireMention({ cfg, groupId: "!room:example.org" })).toBe(
134
+ true,
135
+ );
136
+ expect(
137
+ matrixPlugin.groups.resolveRequireMention({
138
+ cfg,
139
+ accountId: "assistant",
140
+ groupId: "!room:example.org",
141
+ }),
142
+ ).toBe(false);
143
+ });
64
144
  });
package/src/channel.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  } from "./group-mentions.js";
20
20
  import {
21
21
  listMatrixAccountIds,
22
+ resolveMatrixAccountConfig,
22
23
  resolveDefaultMatrixAccountId,
23
24
  resolveMatrixAccount,
24
25
  type ResolvedMatrixAccount,
@@ -31,6 +32,9 @@ import { matrixOnboardingAdapter } from "./onboarding.js";
31
32
  import { matrixOutbound } from "./outbound.js";
32
33
  import { resolveMatrixTargets } from "./resolve-targets.js";
33
34
 
35
+ // Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
36
+ let matrixStartupLock: Promise<void> = Promise.resolve();
37
+
34
38
  const meta = {
35
39
  id: "matrix",
36
40
  label: "Matrix",
@@ -142,19 +146,28 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
142
146
  configured: account.configured,
143
147
  baseUrl: account.homeserver,
144
148
  }),
145
- resolveAllowFrom: ({ cfg }) =>
146
- ((cfg as CoreConfig).channels?.matrix?.dm?.allowFrom ?? []).map((entry) => String(entry)),
149
+ resolveAllowFrom: ({ cfg, accountId }) => {
150
+ const matrixConfig = resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId });
151
+ return (matrixConfig.dm?.allowFrom ?? []).map((entry: string | number) => String(entry));
152
+ },
147
153
  formatAllowFrom: ({ allowFrom }) => normalizeMatrixAllowList(allowFrom),
148
154
  },
149
155
  security: {
150
- resolveDmPolicy: ({ account }) => ({
151
- policy: account.config.dm?.policy ?? "pairing",
152
- allowFrom: account.config.dm?.allowFrom ?? [],
153
- policyPath: "channels.matrix.dm.policy",
154
- allowFromPath: "channels.matrix.dm.allowFrom",
155
- approveHint: formatPairingApproveHint("matrix"),
156
- normalizeEntry: (raw) => normalizeMatrixUserId(raw),
157
- }),
156
+ resolveDmPolicy: ({ account }) => {
157
+ const accountId = account.accountId;
158
+ const prefix =
159
+ accountId && accountId !== "default"
160
+ ? `channels.matrix.accounts.${accountId}.dm`
161
+ : "channels.matrix.dm";
162
+ return {
163
+ policy: account.config.dm?.policy ?? "pairing",
164
+ allowFrom: account.config.dm?.allowFrom ?? [],
165
+ policyPath: `${prefix}.policy`,
166
+ allowFromPath: `${prefix}.allowFrom`,
167
+ approveHint: formatPairingApproveHint("matrix"),
168
+ normalizeEntry: (raw) => normalizeMatrixUserId(raw),
169
+ };
170
+ },
158
171
  collectWarnings: ({ account, cfg }) => {
159
172
  const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy;
160
173
  const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
@@ -171,7 +184,8 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
171
184
  resolveToolPolicy: resolveMatrixGroupToolPolicy,
172
185
  },
173
186
  threading: {
174
- resolveReplyToMode: ({ cfg }) => (cfg as CoreConfig).channels?.matrix?.replyToMode ?? "off",
187
+ resolveReplyToMode: ({ cfg, accountId }) =>
188
+ resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }).replyToMode ?? "off",
175
189
  buildToolContext: ({ context, hasRepliedRef }) => {
176
190
  const currentTarget = context.To;
177
191
  return {
@@ -278,10 +292,10 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
278
292
  .map((id) => ({ kind: "group", id }) as const);
279
293
  return ids;
280
294
  },
281
- listPeersLive: async ({ cfg, query, limit }) =>
282
- listMatrixDirectoryPeersLive({ cfg, query, limit }),
283
- listGroupsLive: async ({ cfg, query, limit }) =>
284
- listMatrixDirectoryGroupsLive({ cfg, query, limit }),
295
+ listPeersLive: async ({ cfg, accountId, query, limit }) =>
296
+ listMatrixDirectoryPeersLive({ cfg, accountId, query, limit }),
297
+ listGroupsLive: async ({ cfg, accountId, query, limit }) =>
298
+ listMatrixDirectoryGroupsLive({ cfg, accountId, query, limit }),
285
299
  },
286
300
  resolver: {
287
301
  resolveTargets: async ({ cfg, inputs, kind, runtime }) =>
@@ -383,9 +397,12 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
383
397
  probe: snapshot.probe,
384
398
  lastProbeAt: snapshot.lastProbeAt ?? null,
385
399
  }),
386
- probeAccount: async ({ timeoutMs, cfg }) => {
400
+ probeAccount: async ({ account, timeoutMs, cfg }) => {
387
401
  try {
388
- const auth = await resolveMatrixAuth({ cfg: cfg as CoreConfig });
402
+ const auth = await resolveMatrixAuth({
403
+ cfg: cfg as CoreConfig,
404
+ accountId: account.accountId,
405
+ });
389
406
  return await probeMatrix({
390
407
  homeserver: auth.homeserver,
391
408
  accessToken: auth.accessToken,
@@ -424,8 +441,32 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
424
441
  baseUrl: account.homeserver,
425
442
  });
426
443
  ctx.log?.info(`[${account.accountId}] starting provider (${account.homeserver ?? "matrix"})`);
444
+
445
+ // Serialize startup: wait for any previous startup to complete import phase.
446
+ // This works around a race condition with concurrent dynamic imports.
447
+ //
448
+ // INVARIANT: The import() below cannot hang because:
449
+ // 1. It only loads local ESM modules with no circular awaits
450
+ // 2. Module initialization is synchronous (no top-level await in ./matrix/index.js)
451
+ // 3. The lock only serializes the import phase, not the provider startup
452
+ const previousLock = matrixStartupLock;
453
+ let releaseLock: () => void = () => {};
454
+ matrixStartupLock = new Promise<void>((resolve) => {
455
+ releaseLock = resolve;
456
+ });
457
+ await previousLock;
458
+
427
459
  // Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
428
- const { monitorMatrixProvider } = await import("./matrix/index.js");
460
+ // Wrap in try/finally to ensure lock is released even if import fails.
461
+ let monitorMatrixProvider: typeof import("./matrix/index.js").monitorMatrixProvider;
462
+ try {
463
+ const module = await import("./matrix/index.js");
464
+ monitorMatrixProvider = module.monitorMatrixProvider;
465
+ } finally {
466
+ // Release lock after import completes or fails
467
+ releaseLock();
468
+ }
469
+
429
470
  return monitorMatrixProvider({
430
471
  runtime: ctx.runtime,
431
472
  abortSignal: ctx.abortSignal,
@@ -0,0 +1,54 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
3
+ import { resolveMatrixAuth } from "./matrix/client.js";
4
+
5
+ vi.mock("./matrix/client.js", () => ({
6
+ resolveMatrixAuth: vi.fn(),
7
+ }));
8
+
9
+ describe("matrix directory live", () => {
10
+ const cfg = { channels: { matrix: {} } };
11
+
12
+ beforeEach(() => {
13
+ vi.mocked(resolveMatrixAuth).mockReset();
14
+ vi.mocked(resolveMatrixAuth).mockResolvedValue({
15
+ homeserver: "https://matrix.example.org",
16
+ userId: "@bot:example.org",
17
+ accessToken: "test-token",
18
+ });
19
+ vi.stubGlobal(
20
+ "fetch",
21
+ vi.fn().mockResolvedValue({
22
+ ok: true,
23
+ json: async () => ({ results: [] }),
24
+ text: async () => "",
25
+ }),
26
+ );
27
+ });
28
+
29
+ afterEach(() => {
30
+ vi.unstubAllGlobals();
31
+ });
32
+
33
+ it("passes accountId to peer directory auth resolution", async () => {
34
+ await listMatrixDirectoryPeersLive({
35
+ cfg,
36
+ accountId: "assistant",
37
+ query: "alice",
38
+ limit: 10,
39
+ });
40
+
41
+ expect(resolveMatrixAuth).toHaveBeenCalledWith({ cfg, accountId: "assistant" });
42
+ });
43
+
44
+ it("passes accountId to group directory auth resolution", async () => {
45
+ await listMatrixDirectoryGroupsLive({
46
+ cfg,
47
+ accountId: "assistant",
48
+ query: "!room:example.org",
49
+ limit: 10,
50
+ });
51
+
52
+ expect(resolveMatrixAuth).toHaveBeenCalledWith({ cfg, accountId: "assistant" });
53
+ });
54
+ });
@@ -50,6 +50,7 @@ function normalizeQuery(value?: string | null): string {
50
50
 
51
51
  export async function listMatrixDirectoryPeersLive(params: {
52
52
  cfg: unknown;
53
+ accountId?: string | null;
53
54
  query?: string | null;
54
55
  limit?: number | null;
55
56
  }): Promise<ChannelDirectoryEntry[]> {
@@ -57,7 +58,7 @@ export async function listMatrixDirectoryPeersLive(params: {
57
58
  if (!query) {
58
59
  return [];
59
60
  }
60
- const auth = await resolveMatrixAuth({ cfg: params.cfg as never });
61
+ const auth = await resolveMatrixAuth({ cfg: params.cfg as never, accountId: params.accountId });
61
62
  const res = await fetchMatrixJson<MatrixUserDirectoryResponse>({
62
63
  homeserver: auth.homeserver,
63
64
  accessToken: auth.accessToken,
@@ -122,6 +123,7 @@ async function fetchMatrixRoomName(
122
123
 
123
124
  export async function listMatrixDirectoryGroupsLive(params: {
124
125
  cfg: unknown;
126
+ accountId?: string | null;
125
127
  query?: string | null;
126
128
  limit?: number | null;
127
129
  }): Promise<ChannelDirectoryEntry[]> {
@@ -129,7 +131,7 @@ export async function listMatrixDirectoryGroupsLive(params: {
129
131
  if (!query) {
130
132
  return [];
131
133
  }
132
- const auth = await resolveMatrixAuth({ cfg: params.cfg as never });
134
+ const auth = await resolveMatrixAuth({ cfg: params.cfg as never, accountId: params.accountId });
133
135
  const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20;
134
136
 
135
137
  if (query.startsWith("#")) {
@@ -1,5 +1,6 @@
1
1
  import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
2
2
  import type { CoreConfig } from "./types.js";
3
+ import { resolveMatrixAccountConfig } from "./matrix/accounts.js";
3
4
  import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
4
5
 
5
6
  export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): boolean {
@@ -18,8 +19,9 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
18
19
  const groupChannel = params.groupChannel?.trim() ?? "";
19
20
  const aliases = groupChannel ? [groupChannel] : [];
20
21
  const cfg = params.cfg as CoreConfig;
22
+ const matrixConfig = resolveMatrixAccountConfig({ cfg, accountId: params.accountId });
21
23
  const resolved = resolveMatrixRoomConfig({
22
- rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
24
+ rooms: matrixConfig.groups ?? matrixConfig.rooms,
23
25
  roomId,
24
26
  aliases,
25
27
  name: groupChannel || undefined,
@@ -56,8 +58,9 @@ export function resolveMatrixGroupToolPolicy(
56
58
  const groupChannel = params.groupChannel?.trim() ?? "";
57
59
  const aliases = groupChannel ? [groupChannel] : [];
58
60
  const cfg = params.cfg as CoreConfig;
61
+ const matrixConfig = resolveMatrixAccountConfig({ cfg, accountId: params.accountId });
59
62
  const resolved = resolveMatrixRoomConfig({
60
- rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
63
+ rooms: matrixConfig.groups ?? matrixConfig.rooms,
61
64
  roomId,
62
65
  aliases,
63
66
  name: groupChannel || undefined,
@@ -1,8 +1,24 @@
1
1
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
2
2
  import type { CoreConfig, MatrixConfig } from "../types.js";
3
- import { resolveMatrixConfig } from "./client.js";
3
+ import { resolveMatrixConfigForAccount } from "./client.js";
4
4
  import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
5
5
 
6
+ /** Merge account config with top-level defaults, preserving nested objects. */
7
+ function mergeAccountConfig(base: MatrixConfig, account: MatrixConfig): MatrixConfig {
8
+ const merged = { ...base, ...account };
9
+ // Deep-merge known nested objects so partial overrides inherit base fields
10
+ for (const key of ["dm", "actions"] as const) {
11
+ const b = base[key];
12
+ const o = account[key];
13
+ if (typeof b === "object" && b != null && typeof o === "object" && o != null) {
14
+ (merged as Record<string, unknown>)[key] = { ...b, ...o };
15
+ }
16
+ }
17
+ // Don't propagate the accounts map into the merged per-account config
18
+ delete (merged as Record<string, unknown>).accounts;
19
+ return merged;
20
+ }
21
+
6
22
  export type ResolvedMatrixAccount = {
7
23
  accountId: string;
8
24
  enabled: boolean;
@@ -13,8 +29,28 @@ export type ResolvedMatrixAccount = {
13
29
  config: MatrixConfig;
14
30
  };
15
31
 
16
- export function listMatrixAccountIds(_cfg: CoreConfig): string[] {
17
- return [DEFAULT_ACCOUNT_ID];
32
+ function listConfiguredAccountIds(cfg: CoreConfig): string[] {
33
+ const accounts = cfg.channels?.matrix?.accounts;
34
+ if (!accounts || typeof accounts !== "object") {
35
+ return [];
36
+ }
37
+ // Normalize and de-duplicate keys so listing and resolution use the same semantics
38
+ return [
39
+ ...new Set(
40
+ Object.keys(accounts)
41
+ .filter(Boolean)
42
+ .map((id) => normalizeAccountId(id)),
43
+ ),
44
+ ];
45
+ }
46
+
47
+ export function listMatrixAccountIds(cfg: CoreConfig): string[] {
48
+ const ids = listConfiguredAccountIds(cfg);
49
+ if (ids.length === 0) {
50
+ // Fall back to default if no accounts configured (legacy top-level config)
51
+ return [DEFAULT_ACCOUNT_ID];
52
+ }
53
+ return ids.toSorted((a, b) => a.localeCompare(b));
18
54
  }
19
55
 
20
56
  export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
@@ -25,20 +61,41 @@ export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
25
61
  return ids[0] ?? DEFAULT_ACCOUNT_ID;
26
62
  }
27
63
 
64
+ function resolveAccountConfig(cfg: CoreConfig, accountId: string): MatrixConfig | undefined {
65
+ const accounts = cfg.channels?.matrix?.accounts;
66
+ if (!accounts || typeof accounts !== "object") {
67
+ return undefined;
68
+ }
69
+ // Direct lookup first (fast path for already-normalized keys)
70
+ if (accounts[accountId]) {
71
+ return accounts[accountId] as MatrixConfig;
72
+ }
73
+ // Fall back to case-insensitive match (user may have mixed-case keys in config)
74
+ const normalized = normalizeAccountId(accountId);
75
+ for (const key of Object.keys(accounts)) {
76
+ if (normalizeAccountId(key) === normalized) {
77
+ return accounts[key] as MatrixConfig;
78
+ }
79
+ }
80
+ return undefined;
81
+ }
82
+
28
83
  export function resolveMatrixAccount(params: {
29
84
  cfg: CoreConfig;
30
85
  accountId?: string | null;
31
86
  }): ResolvedMatrixAccount {
32
87
  const accountId = normalizeAccountId(params.accountId);
33
- const base = params.cfg.channels?.matrix ?? {};
34
- const enabled = base.enabled !== false;
35
- const resolved = resolveMatrixConfig(params.cfg, process.env);
88
+ const matrixBase = params.cfg.channels?.matrix ?? {};
89
+ const base = resolveMatrixAccountConfig({ cfg: params.cfg, accountId });
90
+ const enabled = base.enabled !== false && matrixBase.enabled !== false;
91
+
92
+ const resolved = resolveMatrixConfigForAccount(params.cfg, accountId, process.env);
36
93
  const hasHomeserver = Boolean(resolved.homeserver);
37
94
  const hasUserId = Boolean(resolved.userId);
38
95
  const hasAccessToken = Boolean(resolved.accessToken);
39
96
  const hasPassword = Boolean(resolved.password);
40
97
  const hasPasswordAuth = hasUserId && hasPassword;
41
- const stored = loadMatrixCredentials(process.env);
98
+ const stored = loadMatrixCredentials(process.env, accountId);
42
99
  const hasStored =
43
100
  stored && resolved.homeserver
44
101
  ? credentialsMatchConfig(stored, {
@@ -58,6 +115,21 @@ export function resolveMatrixAccount(params: {
58
115
  };
59
116
  }
60
117
 
118
+ export function resolveMatrixAccountConfig(params: {
119
+ cfg: CoreConfig;
120
+ accountId?: string | null;
121
+ }): MatrixConfig {
122
+ const accountId = normalizeAccountId(params.accountId);
123
+ const matrixBase = params.cfg.channels?.matrix ?? {};
124
+ const accountConfig = resolveAccountConfig(params.cfg, accountId);
125
+ if (!accountConfig) {
126
+ return matrixBase;
127
+ }
128
+ // Merge account-specific config with top-level defaults so settings like
129
+ // groupPolicy and blockStreaming inherit when not overridden.
130
+ return mergeAccountConfig(matrixBase, accountConfig);
131
+ }
132
+
61
133
  export function listEnabledMatrixAccounts(cfg: CoreConfig): ResolvedMatrixAccount[] {
62
134
  return listMatrixAccountIds(cfg)
63
135
  .map((accountId) => resolveMatrixAccount({ cfg, accountId }))
@@ -1,3 +1,4 @@
1
+ import { normalizeAccountId } from "openclaw/plugin-sdk";
1
2
  import type { CoreConfig } from "../../types.js";
2
3
  import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
3
4
  import { getMatrixRuntime } from "../../runtime.js";
@@ -22,7 +23,9 @@ export async function resolveActionClient(
22
23
  if (opts.client) {
23
24
  return { client: opts.client, stopOnDone: false };
24
25
  }
25
- const active = getActiveMatrixClient();
26
+ // Normalize accountId early to ensure consistent keying across all lookups
27
+ const accountId = normalizeAccountId(opts.accountId);
28
+ const active = getActiveMatrixClient(accountId);
26
29
  if (active) {
27
30
  return { client: active, stopOnDone: false };
28
31
  }
@@ -31,11 +34,13 @@ export async function resolveActionClient(
31
34
  const client = await resolveSharedMatrixClient({
32
35
  cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
33
36
  timeoutMs: opts.timeoutMs,
37
+ accountId,
34
38
  });
35
39
  return { client, stopOnDone: false };
36
40
  }
37
41
  const auth = await resolveMatrixAuth({
38
42
  cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
43
+ accountId,
39
44
  });
40
45
  const client = await createMatrixClient({
41
46
  homeserver: auth.homeserver,
@@ -43,6 +48,7 @@ export async function resolveActionClient(
43
48
  accessToken: auth.accessToken,
44
49
  encryption: auth.encryption,
45
50
  localTimeoutMs: opts.timeoutMs,
51
+ accountId,
46
52
  });
47
53
  if (auth.encryption && client.crypto) {
48
54
  try {
@@ -57,6 +57,7 @@ export type MatrixRawEvent = {
57
57
  export type MatrixActionClientOpts = {
58
58
  client?: MatrixClient;
59
59
  timeoutMs?: number;
60
+ accountId?: string | null;
60
61
  };
61
62
 
62
63
  export type MatrixMessageSummary = {
@@ -1,11 +1,32 @@
1
1
  import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+ import { normalizeAccountId } from "openclaw/plugin-sdk";
2
3
 
3
- let activeClient: MatrixClient | null = null;
4
+ // Support multiple active clients for multi-account
5
+ const activeClients = new Map<string, MatrixClient>();
4
6
 
5
- export function setActiveMatrixClient(client: MatrixClient | null): void {
6
- activeClient = client;
7
+ export function setActiveMatrixClient(
8
+ client: MatrixClient | null,
9
+ accountId?: string | null,
10
+ ): void {
11
+ const key = normalizeAccountId(accountId);
12
+ if (client) {
13
+ activeClients.set(key, client);
14
+ } else {
15
+ activeClients.delete(key);
16
+ }
7
17
  }
8
18
 
9
- export function getActiveMatrixClient(): MatrixClient | null {
10
- return activeClient;
19
+ export function getActiveMatrixClient(accountId?: string | null): MatrixClient | null {
20
+ const key = normalizeAccountId(accountId);
21
+ return activeClients.get(key) ?? null;
22
+ }
23
+
24
+ export function getAnyActiveMatrixClient(): MatrixClient | null {
25
+ // Return any available client (for backward compatibility)
26
+ const first = activeClients.values().next();
27
+ return first.done ? null : first.value;
28
+ }
29
+
30
+ export function clearAllActiveMatrixClients(): void {
31
+ activeClients.clear();
11
32
  }