From 9c5d8e3025cd627cdad6bf430f440ae952dceee0 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Fri, 17 Apr 2026 20:00:21 -0400 Subject: [PATCH] fix(agents): preserve prefixed Teams peer ids --- ...subagents.sessions-spawn.lifecycle.test.ts | 10 +-- src/agents/spawn-requester-origin.test.ts | 70 +++++++++++++++++++ src/agents/spawn-requester-origin.ts | 10 +++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts index 4711dbd04b8..a651ba3fead 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts @@ -703,11 +703,11 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => { ).toBe("bot-alpha-dm"); }); - it("sessions_spawn strips conversation: prefix for Teams-style targets", async () => { - const rawConversationId = "19:example-conversation@thread.v2"; - // Teams inbound context sets OriginatingTo to `conversation:`. With the - // generic prefix peeler in extractRequesterPeer, the bound-account lookup - // should still find the binding keyed on the raw conversation id. + it("sessions_spawn strips only the Teams conversation: wrapper", async () => { + const rawConversationId = "a:1:example-conversation@thread.v2"; + // Teams inbound context sets OriginatingTo to `conversation:`. The + // Teams id itself may start with another token-colon segment, so extraction + // must stop after the known wrapper instead of peeling arbitrary prefixes. expect( await executeBoundAccountSpawn({ callId: "call-teams-conversation", diff --git a/src/agents/spawn-requester-origin.test.ts b/src/agents/spawn-requester-origin.test.ts index 06ded35d175..d3863966d6f 100644 --- a/src/agents/spawn-requester-origin.test.ts +++ b/src/agents/spawn-requester-origin.test.ts @@ -39,4 +39,74 @@ describe("resolveRequesterOriginForChild", () => { to, }); }); + + it("preserves canonical peer ids that start with token-colon after a known wrapper", () => { + const to = "conversation:a:1:team-thread"; + const cfg = { + bindings: [ + { + type: "route", + agentId: "bot-alpha", + match: { + channel: "msteams", + peer: { + kind: "channel", + id: "a:1:team-thread", + }, + accountId: "bot-alpha-teams", + }, + }, + ], + } as OpenClawConfig; + + expect( + resolveRequesterOriginForChild({ + cfg, + targetAgentId: "bot-alpha", + requesterAgentId: "main", + requesterChannel: "msteams", + requesterAccountId: "bot-beta", + requesterTo: to, + }), + ).toMatchObject({ + channel: "msteams", + accountId: "bot-alpha-teams", + to, + }); + }); + + it("still peels channel id plus kind wrappers before peer lookup", () => { + const to = "line:group:U123example"; + const cfg = { + bindings: [ + { + type: "route", + agentId: "bot-alpha", + match: { + channel: "line", + peer: { + kind: "group", + id: "U123example", + }, + accountId: "bot-alpha-line", + }, + }, + ], + } as OpenClawConfig; + + expect( + resolveRequesterOriginForChild({ + cfg, + targetAgentId: "bot-alpha", + requesterAgentId: "main", + requesterChannel: "line", + requesterAccountId: "bot-beta", + requesterTo: to, + }), + ).toMatchObject({ + channel: "line", + accountId: "bot-alpha-line", + to, + }); + }); }); diff --git a/src/agents/spawn-requester-origin.ts b/src/agents/spawn-requester-origin.ts index 6977aed89a7..eb7879484d8 100644 --- a/src/agents/spawn-requester-origin.ts +++ b/src/agents/spawn-requester-origin.ts @@ -26,6 +26,13 @@ const KIND_PREFIX_TO_CHAT_TYPE: Readonly> = { // Matches one leading `:` wrapper at a time. const GENERIC_PREFIX_PATTERN = /^[a-z][a-z0-9_-]*:/i; +function shouldPeelRequesterPrefix(prefix: string, channelId: string | undefined): boolean { + if (prefix in KIND_PREFIX_TO_CHAT_TYPE) { + return true; + } + return Boolean(channelId && prefix === `${channelId.trim().toLowerCase()}:`); +} + export function extractRequesterPeer( channelId: string | undefined, requesterTo: string | undefined, @@ -49,6 +56,9 @@ export function extractRequesterPeer( break; } const prefix = match[0].toLowerCase(); + if (!shouldPeelRequesterPrefix(prefix, channelId)) { + break; + } if (prefix in KIND_PREFIX_TO_CHAT_TYPE) { inferredKind ??= KIND_PREFIX_TO_CHAT_TYPE[prefix]; }