diff --git a/CHANGELOG.md b/CHANGELOG.md index 647c67912..c363a6c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### Fixes - Sub-agents: normalize announce delivery origin + queue bucketing by accountId to keep multi-account routing stable. (#1061, #1058) — thanks @adam91holt. +- Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing. - Repo: fix oxlint config filename and move ignore pattern into config. (#1064) — thanks @connorshea. - Messages: `/stop` now hard-aborts queued followups and sub-agent runs; suppress zero-count stop notes. - Sessions: reset `compactionCount` on `/new` and `/reset`, and preserve `sessions.json` file mode (0600). diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 280fb5ec0..8d5544f1a 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -155,6 +155,7 @@ export const agentHandlers: GatewayRequestHandlers = { skillsSnapshot: entry?.skillsSnapshot, lastChannel: entry?.lastChannel, lastTo: entry?.lastTo, + lastAccountId: entry?.lastAccountId, modelOverride: entry?.modelOverride, providerOverride: entry?.providerOverride, label: labelValue, @@ -201,8 +202,11 @@ export const agentHandlers: GatewayRequestHandlers = { const lastChannel = sessionEntry?.lastChannel; const lastTo = typeof sessionEntry?.lastTo === "string" ? sessionEntry.lastTo.trim() : ""; + const explicitTo = + typeof request.to === "string" && request.to.trim() ? request.to.trim() : undefined; const resolvedAccountId = - normalizeAccountId(request.accountId) ?? normalizeAccountId(sessionEntry?.lastAccountId); + normalizeAccountId(request.accountId) ?? + (explicitTo ? undefined : normalizeAccountId(sessionEntry?.lastAccountId)); const wantsDelivery = request.deliver === true; @@ -224,8 +228,6 @@ export const agentHandlers: GatewayRequestHandlers = { return wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL; })(); - const explicitTo = - typeof request.to === "string" && request.to.trim() ? request.to.trim() : undefined; const deliveryTargetMode = explicitTo ? "explicit" : isDeliverableMessageChannel(resolvedChannel) diff --git a/src/gateway/server.agent.gateway-server-agent-a.test.ts b/src/gateway/server.agent.gateway-server-agent-a.test.ts index bc0f75735..cab401a86 100644 --- a/src/gateway/server.agent.gateway-server-agent-a.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-a.test.ts @@ -152,6 +152,95 @@ describe("gateway server agent", () => { testState.allowFrom = undefined; }); + test("agent avoids lastAccountId when explicit to is provided", async () => { + testState.allowFrom = ["+1555"]; + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); + testState.sessionStorePath = path.join(dir, "sessions.json"); + await fs.writeFile( + testState.sessionStorePath, + JSON.stringify( + { + main: { + sessionId: "sess-main-explicit", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "legacy", + }, + }, + null, + 2, + ), + "utf-8", + ); + + const { server, ws } = await startServerWithClient(); + await connectOk(ws); + + const res = await rpcReq(ws, "agent", { + message: "hi", + sessionKey: "main", + deliver: true, + to: "+1666", + idempotencyKey: "idem-agent-explicit", + }); + expect(res.ok).toBe(true); + + const spy = vi.mocked(agentCommand); + const call = spy.mock.calls.at(-1)?.[0] as Record; + expectChannels(call, "whatsapp"); + expect(call.to).toBe("+1666"); + expect(call.accountId).toBeUndefined(); + + ws.close(); + await server.close(); + testState.allowFrom = undefined; + }); + + test("agent falls back to lastAccountId for implicit delivery", async () => { + testState.allowFrom = ["+1555"]; + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); + testState.sessionStorePath = path.join(dir, "sessions.json"); + await fs.writeFile( + testState.sessionStorePath, + JSON.stringify( + { + main: { + sessionId: "sess-main-implicit", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "kev", + }, + }, + null, + 2, + ), + "utf-8", + ); + + const { server, ws } = await startServerWithClient(); + await connectOk(ws); + + const res = await rpcReq(ws, "agent", { + message: "hi", + sessionKey: "main", + deliver: true, + idempotencyKey: "idem-agent-implicit-account", + }); + expect(res.ok).toBe(true); + + const spy = vi.mocked(agentCommand); + const call = spy.mock.calls.at(-1)?.[0] as Record; + expectChannels(call, "whatsapp"); + expect(call.to).toBe("+1555"); + expect(call.accountId).toBe("kev"); + + ws.close(); + await server.close(); + testState.allowFrom = undefined; + }); + test("agent forwards image attachments as images[]", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json");