agents: clarify spawn peer alias lookup

This commit is contained in:
Gustavo Madeira Santana
2026-04-17 13:12:23 -04:00
parent 6b1a3b02c0
commit 17e6d699b1
4 changed files with 39 additions and 23 deletions

View File

@@ -3,7 +3,10 @@ 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", () => {
it.each([
["channel:conversation-a", "channel:conversation-a"],
["thread:conversation-a/thread-a", "thread:conversation-a/thread-a"],
])("keeps canonical prefixed peer id %s eligible for exact binding lookup", (to, peerId) => {
const cfg = {
bindings: [
{
@@ -13,7 +16,7 @@ describe("resolveRequesterOriginForChild", () => {
channel: "qa-channel",
peer: {
kind: "channel",
id: "channel:conversation-a",
id: peerId,
},
accountId: "bot-alpha-qa",
},
@@ -28,12 +31,12 @@ describe("resolveRequesterOriginForChild", () => {
requesterAgentId: "main",
requesterChannel: "qa-channel",
requesterAccountId: "bot-beta",
requesterTo: "channel:conversation-a",
requesterTo: to,
}),
).toMatchObject({
channel: "qa-channel",
accountId: "bot-alpha-qa",
to: "channel:conversation-a",
to,
});
});
});

View File

@@ -4,10 +4,11 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveFirstBoundAccountId } from "../routing/bound-account-read.js";
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
// Delivery targets carry a channel-side prefix (e.g. Matrix uses
// `room:<roomId>`; LINE uses `line:group:<id>`), but route bindings store raw
// peer ids on `match.peer.id`. Peel namespace and kind prefixes so the raw peer
// id surfaces for binding lookup.
// Delivery targets often carry a transport wrapper (e.g. Matrix `room:<id>` or
// LINE `line:group:<id>`), while route bindings commonly store raw peer ids on
// `match.peer.id`. Peel wrappers for those lookups, and separately pass the
// original target as an exact-match alias for channels whose canonical peer ids
// intentionally include prefixes such as `channel:` or `thread:`.
const KIND_PREFIX_TO_CHAT_TYPE: Readonly<Record<string, ChatType>> = {
"room:": "channel",
"channel:": "channel",
@@ -22,10 +23,7 @@ const KIND_PREFIX_TO_CHAT_TYPE: Readonly<Record<string, ChatType>> = {
"pm:": "direct",
};
// Matches any `<alpha-token>:` prefix. Real-world peer ids (Matrix `!`/`@`,
// IRC `#`, Slack/Discord/LINE alphanumerics, numeric Telegram/WhatsApp, or
// email-style `user@server`) never start with a lowercase-alpha token followed
// by `:`, so this peels prefixes without risking the raw id itself.
// Matches one leading `<alpha-token>:` wrapper at a time.
const GENERIC_PREFIX_PATTERN = /^[a-z][a-z0-9_-]*:/i;
export function extractRequesterPeer(
@@ -92,7 +90,7 @@ export function resolveRequesterOriginForChild(params: {
channelId: params.requesterChannel,
agentId: params.targetAgentId,
peerId: normalizedPeerId,
peerIdAliases:
exactPeerIdAliases:
rawPeerIdAlias && rawPeerIdAlias !== normalizedPeerId ? [rawPeerIdAlias] : undefined,
peerKind: inferredPeerKind,
})

View File

@@ -340,7 +340,7 @@ describe("resolveFirstBoundAccountId", () => {
channelId: "qa-channel",
agentId: "bot-alpha",
peerId: "conversation-a",
peerIdAliases: ["channel:conversation-a"],
exactPeerIdAliases: ["channel:conversation-a"],
peerKind: "channel",
}),
).toBe("bot-alpha-conversation");

View File

@@ -56,8 +56,25 @@ function peerKindMatches(a: ChatType, b: ChatType): boolean {
if (a === b) {
return true;
}
const pair = new Set([a, b]);
return pair.has("group") && pair.has("channel");
return (a === "group" && b === "channel") || (a === "channel" && b === "group");
}
function buildExactPeerIdSet(params: {
peerId?: string;
exactPeerIdAliases?: string[];
}): Set<string> {
const exactPeerIds = new Set<string>();
const peerId = params.peerId?.trim();
if (peerId) {
exactPeerIds.add(peerId);
}
for (const alias of params.exactPeerIdAliases ?? []) {
const trimmed = alias.trim();
if (trimmed) {
exactPeerIds.add(trimmed);
}
}
return exactPeerIds;
}
export function resolveFirstBoundAccountId(params: {
@@ -65,7 +82,7 @@ export function resolveFirstBoundAccountId(params: {
channelId: string;
agentId: string;
peerId?: string;
peerIdAliases?: string[];
exactPeerIdAliases?: string[];
peerKind?: ChatType;
}): string | undefined {
const normalizedChannel = normalizeBindingChannelId(params.channelId);
@@ -74,12 +91,10 @@ 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 exactPeerIds = buildExactPeerIdSet({
peerId: normalizedPeerId,
exactPeerIdAliases: params.exactPeerIdAliases,
});
const normalizedPeerKind = normalizeChatType(params.peerKind) ?? undefined;
let wildcardPeerMatch: string | undefined;
let channelOnlyFallback: string | undefined;