fix(matrix): fix multiple Conduit compatibility issues preventing message delivery

## 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
This commit is contained in:
efe-arv 2026-03-02 01:29:26 +03:00 committed by Peter Steinberger
parent 43cad8268d
commit f66f563c1a
4 changed files with 31 additions and 9 deletions

View File

@ -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);

View File

@ -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;
},

View File

@ -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<object>();
function createSelfUserIdResolver(client: Pick<MatrixClient, "getUserId">) {
let selfUserId: string | undefined;
let selfUserIdLookup: Promise<string | undefined> | undefined;
@ -41,6 +47,12 @@ export function registerMatrixMonitorEvents(params: {
formatNativeDependencyHint: PluginRuntime["system"]["formatNativeDependencyHint"];
onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise<void>;
}): void {
if (registeredClients.has(params.client)) {
console.error("[matrix] skipping duplicate listener registration for client");
return;
}
registeredClients.add(params.client);
const {
client,
auth,

View File

@ -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<string>();