From fd7ca4c3945f3cb4cdc94b27ad6a7566e9492215 Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Mon, 23 Feb 2026 02:14:07 +0000 Subject: [PATCH] fix: normalize input peer.kind in resolveAgentRoute (#22730) The input peer.kind from channel plugins was used as-is without normalization via normalizeChatType(), while the binding side correctly normalized. This caused "dm" !== "direct" mismatches in matchesBindingScope, making plugins that use "dm" as peerKind fail to match bindings configured with "direct". Normalize both peer.kind and parentPeer.kind through normalizeChatType() so that "dm" and "direct" are treated equivalently on both sides. Co-Authored-By: Claude Opus 4.6 (cherry picked from commit b0c96702f5531287f857410303b2c3cc698a1441) --- src/routing/resolve-route.test.ts | 24 ++++++++++++++++++++++++ src/routing/resolve-route.ts | 12 ++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/routing/resolve-route.test.ts b/src/routing/resolve-route.test.ts index 5337731f3e2..c92bfe2ba17 100644 --- a/src/routing/resolve-route.test.ts +++ b/src/routing/resolve-route.test.ts @@ -521,6 +521,30 @@ describe("backward compatibility: peer.kind dm → direct", () => { expect(route.agentId).toBe("alex"); expect(route.matchedBy).toBe("binding.peer"); }); + + test("runtime dm peer.kind matches config direct binding (#22730)", () => { + const cfg: OpenClawConfig = { + bindings: [ + { + agentId: "alex", + match: { + channel: "whatsapp", + // Config uses canonical "direct" + peer: { kind: "direct", id: "+15551234567" }, + }, + }, + ], + }; + const route = resolveAgentRoute({ + cfg, + channel: "whatsapp", + accountId: null, + // Plugin sends "dm" instead of "direct" + peer: { kind: "dm" as ChatType, id: "+15551234567" }, + }); + expect(route.agentId).toBe("alex"); + expect(route.matchedBy).toBe("binding.peer"); + }); }); describe("role-based agent routing", () => { diff --git a/src/routing/resolve-route.ts b/src/routing/resolve-route.ts index 6dab84d3420..74f1b3831b4 100644 --- a/src/routing/resolve-route.ts +++ b/src/routing/resolve-route.ts @@ -291,7 +291,12 @@ function matchesBindingScope(match: NormalizedBindingMatch, scope: BindingScope) export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentRoute { const channel = normalizeToken(input.channel); const accountId = normalizeAccountId(input.accountId); - const peer = input.peer ? { kind: input.peer.kind, id: normalizeId(input.peer.id) } : null; + const peer = input.peer + ? { + kind: normalizeChatType(input.peer.kind) ?? input.peer.kind, + id: normalizeId(input.peer.id), + } + : null; const guildId = normalizeId(input.guildId); const teamId = normalizeId(input.teamId); const memberRoleIds = input.memberRoleIds ?? []; @@ -351,7 +356,10 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR } // Thread parent inheritance: if peer (thread) didn't match, check parent peer binding const parentPeer = input.parentPeer - ? { kind: input.parentPeer.kind, id: normalizeId(input.parentPeer.id) } + ? { + kind: normalizeChatType(input.parentPeer.kind) ?? input.parentPeer.kind, + id: normalizeId(input.parentPeer.id), + } : null; const baseScope = { guildId,