fix(agents): generic <word>: prefix peeler for delivery targets

MS Teams inbound context sets OriginatingTo to `conversation:<id>` while
route bindings key on the bare conversationId. The previous hand-rolled
prefix list (`room:`, `channel:`, `chat:`, `group:`, `user:`, `dm:`)
missed `conversation:` (and any future channel-specific namespacing), so
Teams cross-agent subagent spawns fell back to channel-only/caller
account and posted from the wrong identity.

extractRequesterPeer now uses a generic `^[a-z][a-z0-9_-]*:` regex to
peel any lowercase-alpha token-colon prefix, looping until the raw peer
id surfaces. Real-world peer ids never start with a lowercase-alpha
token followed by `:` (Matrix uses `!`/`@`, IRC `#`, Slack/Discord/LINE
alphanumerics, numeric Telegram/WhatsApp, or email-style `user@server`),
so this is safe. Known prefixes are mapped to ChatType for peerKind
inference (`conversation:`/`room:`/`channel:`/`chat:`/`thread:`/`topic:`
→ channel, `group:`/`team:` → group, `user:`/`dm:`/`pm:` → direct).

Regression test: sessions_spawn strips `conversation:` prefix for
Teams-style targets — a binding keyed on the raw conversation id
resolves correctly when the caller's `agentTo` is `conversation:<id>`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Luke Boyett
2026-04-16 09:44:03 -04:00
committed by Gustavo Madeira Santana
parent 3824ee0d6f
commit 2efc727406
2 changed files with 51 additions and 0 deletions

View File

@@ -754,6 +754,51 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
expect(spawnAccountId).toBe("bot-alpha-line");
});
it("sessions_spawn strips conversation: prefix for Teams-style targets", async () => {
let spawnAccountId: string | undefined;
const rawConversationId = "19:example-conversation@thread.v2";
setSessionsSpawnConfigOverride({
session: { mainKey: "main", scope: "per-sender" },
messages: { queue: { debounceMs: 0 } },
agents: { defaults: { subagents: { allowAgents: ["bot-alpha"] } } },
bindings: [
{
type: "route",
agentId: "bot-alpha",
match: {
channel: "msteams",
peer: { kind: "channel", id: rawConversationId },
accountId: "bot-alpha-teams",
},
},
],
});
setupSessionsSpawnGatewayMock({
onAgentSubagentSpawn: (params) => {
const rec = params as { accountId?: string } | undefined;
spawnAccountId = rec?.accountId;
},
});
// Teams inbound context sets OriginatingTo to `conversation:<id>`. With the
// generic prefix peeler in extractRequesterPeer, the bound-account lookup
// should still find the binding keyed on the raw conversation id.
const tool = await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "msteams",
agentAccountId: "bot-beta",
agentTo: `conversation:${rawConversationId}`,
});
const result = await tool.execute("call-teams-conversation", {
task: "do thing",
agentId: "bot-alpha",
cleanup: "keep",
});
expect(result.details).toMatchObject({ status: "accepted", runId: expect.any(String) });
expect(spawnAccountId).toBe("bot-alpha-teams");
});
it("sessions_spawn preserves the caller's account for same-agent subagent spawns", async () => {
let spawnAccountId: string | undefined;
const room = "!someRoom:example.org";

View File

@@ -308,6 +308,12 @@ 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 cleanly peels namespace and kind prefixes without risking
// the raw id itself. When a peeled token maps to a known ChatType, we also
// record it as an inferred peerKind.
const GENERIC_PREFIX_PATTERN = /^[a-z][a-z0-9_-]*:/i;
function extractRequesterPeer(