diff --git a/extensions/matrix/src/matrix/client/shared.ts b/extensions/matrix/src/matrix/client/shared.ts index c04c61829ab..45e07ad4ed9 100644 --- a/extensions/matrix/src/matrix/client/shared.ts +++ b/extensions/matrix/src/matrix/client/shared.ts @@ -84,7 +84,15 @@ async function ensureSharedClientStarted(params: { } } - await client.start(); + // bot-sdk start() returns a promise that never resolves (infinite sync loop). + // Fire-and-forget: the sync loop runs, events fire on the client, + // but we must not await or the entire provider startup hangs. + // See: https://github.com/openclaw/openclaw/issues/NEW + client.start().catch((err: unknown) => { + LogService.error("MatrixClientLite", "client.start() error:", err); + }); + // Give the sync loop a moment to initialize + await new Promise(resolve => setTimeout(resolve, 2000)); params.state.started = true; })(); sharedClientStartPromises.set(key, startPromise); diff --git a/extensions/matrix/src/matrix/monitor/direct.ts b/extensions/matrix/src/matrix/monitor/direct.ts index 5cd6e88758e..4506bd7d80e 100644 --- a/extensions/matrix/src/matrix/monitor/direct.ts +++ b/extensions/matrix/src/matrix/monitor/direct.ts @@ -78,17 +78,13 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr const { roomId, senderId } = params; await refreshDmCache(); + // Check m.direct account data (most authoritative) if (client.dms.isDm(roomId)) { log(`matrix: dm detected via m.direct room=${roomId}`); return true; } - const memberCount = await resolveMemberCount(roomId); - if (memberCount === 2) { - log(`matrix: dm detected via member count room=${roomId} members=${memberCount}`); - return true; - } - + // Check m.room.member state for is_direct flag const selfUserId = params.selfUserId ?? (await ensureSelfUserId()); const directViaState = (await hasDirectFlag(roomId, senderId)) || (await hasDirectFlag(roomId, selfUserId ?? "")); @@ -97,6 +93,12 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr return true; } + // Member count alone is NOT a reliable DM indicator. + // Explicitly configured group rooms with 2 members (e.g. bot + one user) + // were being misclassified as DMs, causing messages to be routed through + // DM policy instead of group policy and silently dropped. + // See: https://github.com/openclaw/openclaw/issues/20145 + const memberCount = await resolveMemberCount(roomId); log(`matrix: dm check room=${roomId} result=group members=${memberCount ?? "unknown"}`); return false; }, diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index 279517d521d..a8519dcb734 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -5,6 +5,12 @@ import { sendReadReceiptMatrix } from "../send.js"; import type { MatrixRawEvent } from "./types.js"; import { EventType } from "./types.js"; +// Track which clients have had monitor events registered to prevent +// duplicate listener registration when the plugin loads twice +// (e.g. bundled channel + extension both try to start). +// See: https://github.com/openclaw/openclaw/issues/18330 +const registeredClients = new WeakSet(); + function createSelfUserIdResolver(client: Pick) { let selfUserId: string | undefined; let selfUserIdLookup: Promise | undefined; @@ -41,6 +47,12 @@ export function registerMatrixMonitorEvents(params: { formatNativeDependencyHint: PluginRuntime["system"]["formatNativeDependencyHint"]; onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise; }): void { + if (registeredClients.has(params.client)) { + console.error("[matrix] skipping duplicate listener registration for client"); + return; + } + registeredClients.add(params.client); + const { client, auth, diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 936eabdd346..f282cf798a3 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -153,7 +153,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi continue; } const cleaned = normalizeRoomEntry(trimmed); - if ((cleaned.startsWith("!") || cleaned.startsWith("#")) && cleaned.includes(":")) { + if (cleaned.startsWith("!") || (cleaned.startsWith("#") && cleaned.includes(":"))) { if (!nextRooms[cleaned]) { nextRooms[cleaned] = roomConfig; } @@ -268,7 +268,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const mediaMaxMb = opts.mediaMaxMb ?? accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB; const mediaMaxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024; const startupMs = Date.now(); - const startupGraceMs = 0; + const startupGraceMs = 5000; // 5s grace for slow homeservers (e.g. Conduit filter M_NOT_FOUND retry) const directTracker = createDirectRoomTracker(client, { log: logVerboseMessage }); registerMatrixAutoJoin({ client, cfg, runtime }); const warnedEncryptedRooms = new Set();