From a0ed4273ee688aba24630dff4ac443ee5acb2e61 Mon Sep 17 00:00:00 2001 From: Yzx <53250620+849261680@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:14:17 +0800 Subject: [PATCH] fix(agents): resolve bound route agent for inbound sessions (#95118) --- src/agents/agent-scope.ts | 7 ++++- ...gent-runner.resolvesessionagentids.test.ts | 28 +++++++++++++++++++ src/auto-reply/reply/dispatch-from-config.ts | 4 +-- src/auto-reply/reply/get-reply.ts | 2 +- src/auto-reply/reply/session.ts | 1 + src/channels/inbound-event/context.test.ts | 15 ++++++++++ src/channels/inbound-event/context.ts | 1 + 7 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index 2dbf013f82f9..254dcd566447 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -301,6 +301,7 @@ export function resolveSessionAgentIds(params: { sessionKey?: string; config?: OpenClawConfig; agentId?: string; + fallbackAgentId?: string; }): { defaultAgentId: string; sessionAgentId: string; @@ -308,11 +309,14 @@ export function resolveSessionAgentIds(params: { const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); const explicitAgentIdRaw = normalizeLowercaseStringOrEmpty(params.agentId); const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null; + const fallbackAgentIdRaw = normalizeLowercaseStringOrEmpty(params.fallbackAgentId); + const fallbackAgentId = fallbackAgentIdRaw ? normalizeAgentId(fallbackAgentIdRaw) : null; const sessionKey = params.sessionKey?.trim(); const normalizedSessionKey = sessionKey ? normalizeLowercaseStringOrEmpty(sessionKey) : undefined; const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null; const sessionAgentId = - explicitAgentId ?? (parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId); + explicitAgentId ?? + (parsed?.agentId ? normalizeAgentId(parsed.agentId) : (fallbackAgentId ?? defaultAgentId)); return { defaultAgentId, sessionAgentId }; } @@ -320,6 +324,7 @@ export function resolveSessionAgentId(params: { sessionKey?: string; config?: OpenClawConfig; agentId?: string; + fallbackAgentId?: string; }): string { return resolveSessionAgentIds(params).sessionAgentId; } diff --git a/src/agents/embedded-agent-runner.resolvesessionagentids.test.ts b/src/agents/embedded-agent-runner.resolvesessionagentids.test.ts index 35bf3d57de8c..aac6284db954 100644 --- a/src/agents/embedded-agent-runner.resolvesessionagentids.test.ts +++ b/src/agents/embedded-agent-runner.resolvesessionagentids.test.ts @@ -68,4 +68,32 @@ describe("resolveSessionAgentIds", () => { }); expect(sessionAgentId).toBe("main"); }); + + it("uses fallbackAgentId for unscoped channel session keys", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + sessionKey: "feishu:direct:ou_user1", + fallbackAgentId: "main", + config: cfg, + }); + expect(sessionAgentId).toBe("main"); + }); + + it("prefers session-key agent over fallbackAgentId", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + sessionKey: "agent:beta:feishu:direct:ou_user1", + fallbackAgentId: "main", + config: cfg, + }); + expect(sessionAgentId).toBe("beta"); + }); + + it("prefers explicit agentId over fallbackAgentId", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + sessionKey: "feishu:direct:ou_user1", + agentId: "beta", + fallbackAgentId: "main", + config: cfg, + }); + expect(sessionAgentId).toBe("beta"); + }); }); diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index e79f40d13568..f2d19f5fc5a3 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -394,7 +394,7 @@ const resolveSessionStoreLookup = ( if (!sessionKey) { return {}; } - const agentId = resolveSessionAgentId({ sessionKey, config: cfg, agentId: ctx.AgentId }); + const agentId = resolveSessionAgentId({ sessionKey, config: cfg, fallbackAgentId: ctx.AgentId }); const storePath = resolveStorePath(cfg.session?.store, { agentId }); try { const store = loadSessionStore(storePath); @@ -1275,7 +1275,7 @@ export async function dispatchReplyFromConfig( const sessionAgentId = resolveSessionAgentId({ sessionKey: acpDispatchSessionKey, config: cfg, - agentId: ctx.AgentId, + fallbackAgentId: ctx.AgentId, }); const sessionAgentCfg = resolveAgentConfig(cfg, sessionAgentId); const verboseProgress = createShouldEmitVerboseProgress({ diff --git a/src/auto-reply/reply/get-reply.ts b/src/auto-reply/reply/get-reply.ts index 058a80e2db7e..8b2e89fc6a5c 100644 --- a/src/auto-reply/reply/get-reply.ts +++ b/src/auto-reply/reply/get-reply.ts @@ -284,7 +284,7 @@ export async function getReplyFromConfig( agentId: resolveSessionAgentId({ sessionKey: resolvedAgentSessionKey, config: cfg, - agentId: finalized.AgentId, + fallbackAgentId: finalized.AgentId, }), }; }, diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 27358c967d90..d3d6125ae238 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -265,6 +265,7 @@ export async function initSessionState(params: { const agentId = resolveSessionAgentId({ sessionKey: sessionCtxForState.SessionKey, config: cfg, + fallbackAgentId: sessionCtxForState.AgentId, }); const groupResolution = resolveGroupSessionKey(sessionCtxForState) ?? undefined; const resetTriggers = sessionCfg?.resetTriggers?.length diff --git a/src/channels/inbound-event/context.test.ts b/src/channels/inbound-event/context.test.ts index 316330a37157..103bea55dc46 100644 --- a/src/channels/inbound-event/context.test.ts +++ b/src/channels/inbound-event/context.test.ts @@ -137,6 +137,7 @@ describe("buildChannelInboundEventContext", () => { From: "test:user:u1", To: "test:room:room-1", SessionKey: "agent:main:test:group:room-1", + AgentId: "main", AccountId: "acct", ParentSessionKey: "agent:main:test:group", ModelParentSessionKey: "agent:main:test:model", @@ -211,6 +212,20 @@ describe("buildChannelInboundEventContext", () => { expect(ctx.CommandAuthorized).toBe(false); }); + it("carries the routed agent for unscoped session keys", async () => { + const ctx = buildChannelInboundEventContext( + createBaseContextParams({ + route: { + agentId: "bound-agent", + routeSessionKey: "feishu:direct:ou_user1", + }, + }), + ); + + expect(ctx.AgentId).toBe("bound-agent"); + expect(ctx.SessionKey).toBe("feishu:direct:ou_user1"); + }); + it("carries room event semantics into the finalized context", async () => { const ctx = buildChannelInboundEventContext( createBaseContextParams({ diff --git a/src/channels/inbound-event/context.ts b/src/channels/inbound-event/context.ts index 5bb636d81333..679fb091cf33 100644 --- a/src/channels/inbound-event/context.ts +++ b/src/channels/inbound-event/context.ts @@ -484,6 +484,7 @@ export function buildChannelInboundEventContext( From: params.from, To: params.reply.to, SessionKey: params.route.dispatchSessionKey ?? params.route.routeSessionKey, + AgentId: params.route.agentId, AccountId: params.route.accountId ?? params.accountId, ParentSessionKey: params.route.parentSessionKey, ModelParentSessionKey: params.route.modelParentSessionKey,