From f66f563c1ae48798ba160f8ba442c166333de65e Mon Sep 17 00:00:00 2001 From: efe-arv Date: Mon, 2 Mar 2026 01:29:26 +0300 Subject: [PATCH] fix(matrix): fix multiple Conduit compatibility issues preventing message delivery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes ### 1. Fix client.start() hanging forever (shared.ts) The bot-sdk's `client.start()` returns a promise that never resolves (infinite sync loop). The plugin awaited it, blocking the entire provider startup — `logged in as` never printed, no messages were processed. Fix: fire-and-forget with error handler + 2s initialization delay. ### 2. Fix DM false positive for 2-member rooms (direct.ts) `memberCount === 2` heuristic misclassified explicitly configured group rooms as DMs when only bot + one user were joined. Messages were routed through DM policy and silently dropped. Fix: remove member count heuristic; only trust `m.direct` account data and `is_direct` room state flag. Ref: #20145 ### 3. Prevent duplicate event listener registration (events.ts) When both bundled channel plugin and extension load, listeners were registered twice on the same shared client, causing inconsistent state. Fix: WeakSet guard to skip registration if client already has listeners. Ref: #18330 ### 4. Add startup grace period (index.ts) `startupGraceMs = 0` dropped messages timestamped during async setup. Especially problematic with Conduit which retries on `M_NOT_FOUND` during filter creation. Fix: 5-second grace period. ### 5. Fix room ID case sensitivity with Conduit (index.ts) Room IDs (`!xyz`) without `:server` suffix failed the `includes(':')` check and were sent to `resolveMatrixTargets`, which called Conduit's `resolveRoom` — returning lowercased IDs. The bot-sdk emits events with original-case IDs, causing config lookup mismatches and reply delivery failures (`M_UNKNOWN: non-create event for room of unknown version`). Fix: treat `!`-prefixed entries as room IDs directly (skip resolution). Only resolve `#alias:server` entries. ## Testing Tested with Conduit homeserver (lightweight Rust Matrix server). All fixes verified with gateway log tracing: - `logged in as @arvi:matrix.local` — first successful login - `room.message` events fire and reach handler - Room config matching returns `allowed: true` - Agent generates response and delivers it to Matrix room --- extensions/matrix/src/matrix/client/shared.ts | 10 +++++++++- extensions/matrix/src/matrix/monitor/direct.ts | 14 ++++++++------ extensions/matrix/src/matrix/monitor/events.ts | 12 ++++++++++++ extensions/matrix/src/matrix/monitor/index.ts | 4 ++-- 4 files changed, 31 insertions(+), 9 deletions(-) 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();