@openclaw/matrix 2026.2.9 → 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.
@@ -1,4 +1,5 @@
1
1
  import { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
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";
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";
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);
@@ -12,6 +12,7 @@ import {
12
12
  } from "openclaw/plugin-sdk";
13
13
  import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js";
14
14
  import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
15
+ import { fetchEventSummary } from "../actions/summary.js";
15
16
  import {
16
17
  formatPollAsText,
17
18
  isPollStartType,
@@ -67,6 +68,7 @@ export type MatrixMonitorHandlerParams = {
67
68
  roomId: string,
68
69
  ) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
69
70
  getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
71
+ accountId?: string | null;
70
72
  };
71
73
 
72
74
  export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
@@ -92,6 +94,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
92
94
  directTracker,
93
95
  getRoomInfo,
94
96
  getMemberDisplayName,
97
+ accountId,
95
98
  } = params;
96
99
 
97
100
  return async (roomId: string, event: MatrixRawEvent) => {
@@ -431,16 +434,66 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
431
434
  isThreadRoot: false, // @vector-im/matrix-bot-sdk doesn't have this info readily available
432
435
  });
433
436
 
434
- const route = core.channel.routing.resolveAgentRoute({
437
+ const baseRoute = core.channel.routing.resolveAgentRoute({
435
438
  cfg,
436
439
  channel: "matrix",
440
+ accountId,
437
441
  peer: {
438
442
  kind: isDirectMessage ? "direct" : "channel",
439
443
  id: isDirectMessage ? senderId : roomId,
440
444
  },
441
445
  });
446
+
447
+ const route = {
448
+ ...baseRoute,
449
+ sessionKey: threadRootId
450
+ ? `${baseRoute.sessionKey}:thread:${threadRootId}`
451
+ : baseRoute.sessionKey,
452
+ };
453
+
454
+ let threadStarterBody: string | undefined;
455
+ let threadLabel: string | undefined;
456
+ let parentSessionKey: string | undefined;
457
+
458
+ if (threadRootId) {
459
+ const existingSession = core.channel.session.readSessionUpdatedAt({
460
+ storePath: core.channel.session.resolveStorePath(cfg.session?.store, {
461
+ agentId: baseRoute.agentId,
462
+ }),
463
+ sessionKey: route.sessionKey,
464
+ });
465
+
466
+ if (existingSession === undefined) {
467
+ try {
468
+ const rootEvent = await fetchEventSummary(client, roomId, threadRootId);
469
+ if (rootEvent?.body) {
470
+ const rootSenderName = rootEvent.sender
471
+ ? await getMemberDisplayName(roomId, rootEvent.sender)
472
+ : undefined;
473
+
474
+ threadStarterBody = core.channel.reply.formatAgentEnvelope({
475
+ channel: "Matrix",
476
+ from: rootSenderName ?? rootEvent.sender ?? "Unknown",
477
+ timestamp: rootEvent.timestamp,
478
+ envelope: core.channel.reply.resolveEnvelopeFormatOptions(cfg),
479
+ body: rootEvent.body,
480
+ });
481
+
482
+ threadLabel = `Matrix thread in ${roomName ?? roomId}`;
483
+ parentSessionKey = baseRoute.sessionKey;
484
+ }
485
+ } catch (err) {
486
+ logVerboseMessage(
487
+ `matrix: failed to fetch thread root ${threadRootId}: ${String(err)}`,
488
+ );
489
+ }
490
+ }
491
+ }
492
+
442
493
  const envelopeFrom = isDirectMessage ? senderName : (roomName ?? roomId);
443
- const textWithId = `${bodyText}\n[matrix event id: ${messageId} room: ${roomId}]`;
494
+ const textWithId = threadRootId
495
+ ? `${bodyText}\n[matrix event id: ${messageId} room: ${roomId} thread: ${threadRootId}]`
496
+ : `${bodyText}\n[matrix event id: ${messageId} room: ${roomId}]`;
444
497
  const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
445
498
  agentId: route.agentId,
446
499
  });
@@ -461,13 +514,14 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
461
514
  const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined;
462
515
  const ctxPayload = core.channel.reply.finalizeInboundContext({
463
516
  Body: body,
517
+ BodyForAgent: bodyText,
464
518
  RawBody: bodyText,
465
519
  CommandBody: bodyText,
466
520
  From: isDirectMessage ? `matrix:${senderId}` : `matrix:channel:${roomId}`,
467
521
  To: `room:${roomId}`,
468
522
  SessionKey: route.sessionKey,
469
523
  AccountId: route.accountId,
470
- ChatType: isDirectMessage ? "direct" : "channel",
524
+ ChatType: threadRootId ? "thread" : isDirectMessage ? "direct" : "channel",
471
525
  ConversationLabel: envelopeFrom,
472
526
  SenderName: senderName,
473
527
  SenderId: senderId,
@@ -490,6 +544,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
490
544
  CommandSource: "text" as const,
491
545
  OriginatingChannel: "matrix" as const,
492
546
  OriginatingTo: `room:${roomId}`,
547
+ ThreadStarterBody: threadStarterBody,
548
+ ThreadLabel: threadLabel,
549
+ ParentSessionKey: parentSessionKey,
493
550
  });
494
551
 
495
552
  await core.channel.session.recordInboundSession({
@@ -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
  };
@@ -30,11 +30,13 @@ async function fetchMatrixMediaBuffer(params: {
30
30
  // Use the client's download method which handles auth
31
31
  try {
32
32
  const result = await params.client.downloadContent(params.mxcUrl);
33
- const buffer = result.data;
33
+ const raw = result.data ?? result;
34
+ const buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
35
+
34
36
  if (buffer.byteLength > params.maxBytes) {
35
37
  throw new Error("Matrix media exceeds configured size limit");
36
38
  }
37
- return { buffer: Buffer.from(buffer) };
39
+ return { buffer, headerType: result.contentType };
38
40
  } catch (err) {
39
41
  throw new Error(`Matrix media download failed: ${String(err)}`, { cause: err });
40
42
  }