diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 7e4c0b3556b..235a977aaf8 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -25,6 +25,7 @@ const mocks = vi.hoisted(() => ({ replaceSubagentRunAfterSteer: vi.fn(), resolveExplicitAgentSessionKey: vi.fn(), resolveBareResetBootstrapFileAccess: vi.fn(() => true), + listAgentIds: vi.fn(() => ["main"]), loadConfigReturn: {} as Record, })); @@ -71,7 +72,8 @@ vi.mock("../../config/config.js", async () => { }); vi.mock("../../agents/agent-scope.js", () => ({ - listAgentIds: () => ["main"], + listAgentIds: mocks.listAgentIds, + resolveDefaultAgentId: () => "main", resolveAgentWorkspaceDir: (cfg: { agents?: { defaults?: { workspace?: string } } }) => cfg?.agents?.defaults?.workspace ?? "/tmp/workspace", resolveAgentEffectiveModelPrimary: () => undefined, @@ -337,6 +339,7 @@ describe("gateway agent handler", () => { resetTaskRegistryForTests(); mocks.resolveExplicitAgentSessionKey.mockReset().mockReturnValue(undefined); mocks.resolveBareResetBootstrapFileAccess.mockReset().mockReturnValue(true); + mocks.listAgentIds.mockReset().mockReturnValue(["main"]); }); it("preserves ACP metadata from the current stored session entry", async () => { @@ -1027,6 +1030,76 @@ describe("gateway agent handler", () => { expect(call?.sessionKey).toBeUndefined(); }); + it("treats whitespace sessionId as absent before resolving the agent session key", async () => { + mocks.resolveExplicitAgentSessionKey.mockReturnValue("agent:main:main"); + mockMainSessionEntry({ sessionId: "existing-session-id" }); + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + await invokeAgent( + { + message: "resume main", + agentId: "main", + sessionId: " ", + idempotencyKey: "blank-session-id-agent-resume", + }, + { reqId: "blank-session-id-agent-resume" }, + ); + + await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); + const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as { + agentId?: string; + sessionId?: string; + sessionKey?: string; + }; + expect(call?.agentId).toBe("main"); + expect(call?.sessionId).toBe("existing-session-id"); + expect(call?.sessionKey).toBe("agent:main:main"); + }); + + it("does not forward a non-main agent id with canonical global session keys", async () => { + mocks.listAgentIds.mockReturnValue(["main", "ops"]); + mocks.resolveExplicitAgentSessionKey.mockReturnValue("agent:ops:main"); + mocks.loadSessionEntry.mockReturnValue({ + cfg: { session: { scope: "global" } }, + storePath: "/tmp/sessions.json", + entry: { + sessionId: "global-session-id", + updatedAt: Date.now(), + }, + canonicalKey: "global", + }); + mocks.updateSessionStore.mockImplementation(async (_path, updater) => { + const store: Record = { + global: { sessionId: "global-session-id", updatedAt: Date.now() }, + }; + return await updater(store); + }); + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + await invokeAgent( + { + message: "global session", + agentId: "ops", + idempotencyKey: "global-session-agent-id", + }, + { reqId: "global-session-agent-id" }, + ); + + await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); + const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as { + agentId?: string; + sessionKey?: string; + }; + expect(call?.agentId).toBeUndefined(); + expect(call?.sessionKey).toBe("global"); + }); + it("dispatches async gateway agent task creation through the detached task runtime seam", async () => { await withTempDir({ prefix: "openclaw-gateway-agent-seam-" }, async (root) => { process.env.OPENCLAW_STATE_DIR = root; diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 419c2f90568..6e235d06968 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -500,9 +500,10 @@ export const agentHandlers: GatewayRequestHandlers = { ); return; } + const requestedSessionId = normalizeOptionalString(request.sessionId); let requestedSessionKey = requestedSessionKeyRaw ?? - (!request.sessionId + (!requestedSessionId ? resolveExplicitAgentSessionKey({ cfg, agentId, @@ -522,7 +523,7 @@ export const agentHandlers: GatewayRequestHandlers = { return; } } - let resolvedSessionId = normalizeOptionalString(request.sessionId); + let resolvedSessionId = requestedSessionId; let sessionEntry: SessionEntry | undefined; let bestEffortDeliver = requestedBestEffortDeliver ?? false; let cfgForAgent: OpenClawConfig | undefined; @@ -915,13 +916,18 @@ export const agentHandlers: GatewayRequestHandlers = { } const resolvedThreadId = explicitThreadId ?? deliveryPlan.resolvedThreadId; + const ingressAgentId = + agentId && + (!resolvedSessionKey || resolveAgentIdFromSessionKey(resolvedSessionKey) === agentId) + ? agentId + : undefined; dispatchAgentRunFromGateway({ ingressOpts: { message, images, imageOrder, - agentId, + agentId: ingressAgentId, provider: providerOverride, model: modelOverride, to: resolvedTo,