From 6b1a3b02c0fe5e27e1cfd9694647ce08b53acf00 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Fri, 17 Apr 2026 13:04:05 -0400 Subject: [PATCH] agents: preserve canonical spawn peer ids --- src/agents/spawn-requester-origin.test.ts | 39 +++++++++++++++++++++++ src/agents/spawn-requester-origin.ts | 3 ++ src/routing/bound-account-read.test.ts | 33 +++++++++++++++++++ src/routing/bound-account-read.ts | 9 +++++- 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/agents/spawn-requester-origin.test.ts diff --git a/src/agents/spawn-requester-origin.test.ts b/src/agents/spawn-requester-origin.test.ts new file mode 100644 index 00000000000..2dceddc425d --- /dev/null +++ b/src/agents/spawn-requester-origin.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { resolveRequesterOriginForChild } from "./spawn-requester-origin.js"; + +describe("resolveRequesterOriginForChild", () => { + it("keeps canonical prefixed peer ids eligible for exact binding lookup", () => { + const cfg = { + bindings: [ + { + type: "route", + agentId: "bot-alpha", + match: { + channel: "qa-channel", + peer: { + kind: "channel", + id: "channel:conversation-a", + }, + accountId: "bot-alpha-qa", + }, + }, + ], + } as OpenClawConfig; + + expect( + resolveRequesterOriginForChild({ + cfg, + targetAgentId: "bot-alpha", + requesterAgentId: "main", + requesterChannel: "qa-channel", + requesterAccountId: "bot-beta", + requesterTo: "channel:conversation-a", + }), + ).toMatchObject({ + channel: "qa-channel", + accountId: "bot-alpha-qa", + to: "channel:conversation-a", + }); + }); +}); diff --git a/src/agents/spawn-requester-origin.ts b/src/agents/spawn-requester-origin.ts index 27d0c910ba4..f20a9519c8d 100644 --- a/src/agents/spawn-requester-origin.ts +++ b/src/agents/spawn-requester-origin.ts @@ -81,6 +81,7 @@ export function resolveRequesterOriginForChild(params: { params.requesterChannel, params.requesterTo, ); + const rawPeerIdAlias = params.requesterTo?.trim(); // Same-agent spawns must keep the caller's active inbound account, not // re-resolve via bindings that may select a different account for the same // agent/channel. @@ -91,6 +92,8 @@ export function resolveRequesterOriginForChild(params: { channelId: params.requesterChannel, agentId: params.targetAgentId, peerId: normalizedPeerId, + peerIdAliases: + rawPeerIdAlias && rawPeerIdAlias !== normalizedPeerId ? [rawPeerIdAlias] : undefined, peerKind: inferredPeerKind, }) : undefined; diff --git a/src/routing/bound-account-read.test.ts b/src/routing/bound-account-read.test.ts index 65523be6b56..f422f66842f 100644 --- a/src/routing/bound-account-read.test.ts +++ b/src/routing/bound-account-read.test.ts @@ -313,6 +313,39 @@ describe("resolveFirstBoundAccountId", () => { ).toBe("bot-alpha-room"); }); + it("matches exact canonical peer aliases before falling back to wildcard bindings", () => { + const cfg = cfgWithBindings([ + { + type: "route", + agentId: "bot-alpha", + match: { + channel: "qa-channel", + peer: { kind: "channel", id: "*" }, + accountId: "bot-alpha-wildcard", + }, + }, + { + type: "route", + agentId: "bot-alpha", + match: { + channel: "qa-channel", + peer: { kind: "channel", id: "channel:conversation-a" }, + accountId: "bot-alpha-conversation", + }, + }, + ]); + expect( + resolveFirstBoundAccountId({ + cfg, + channelId: "qa-channel", + agentId: "bot-alpha", + peerId: "conversation-a", + peerIdAliases: ["channel:conversation-a"], + peerKind: "channel", + }), + ).toBe("bot-alpha-conversation"); + }); + it("skips peer-specific bindings whose kind does not match the caller's peerKind", () => { const cfg = cfgWithBindings([ { diff --git a/src/routing/bound-account-read.ts b/src/routing/bound-account-read.ts index 18fd6d11953..8ee6b68b612 100644 --- a/src/routing/bound-account-read.ts +++ b/src/routing/bound-account-read.ts @@ -65,6 +65,7 @@ export function resolveFirstBoundAccountId(params: { channelId: string; agentId: string; peerId?: string; + peerIdAliases?: string[]; peerKind?: ChatType; }): string | undefined { const normalizedChannel = normalizeBindingChannelId(params.channelId); @@ -73,6 +74,12 @@ export function resolveFirstBoundAccountId(params: { } const normalizedAgentId = normalizeAgentId(params.agentId); const normalizedPeerId = params.peerId?.trim() || undefined; + const exactPeerIds = new Set( + [ + normalizedPeerId, + ...(params.peerIdAliases ?? []).map((value) => value.trim()).filter(Boolean), + ].filter((value): value is string => Boolean(value)), + ); const normalizedPeerKind = normalizeChatType(params.peerKind) ?? undefined; let wildcardPeerMatch: string | undefined; let channelOnlyFallback: string | undefined; @@ -121,7 +128,7 @@ export function resolveFirstBoundAccountId(params: { ) { continue; } - if (normalizedPeerId && resolved.peerId === normalizedPeerId) { + if (exactPeerIds.has(resolved.peerId)) { return resolved.accountId; } if (!normalizedPeerId) {