From 35083fcb3751a515e847a01015fee71e020c5320 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 22:32:59 +0100 Subject: [PATCH] fix(gateway): avoid whatsapp fallback for internal runs --- src/agents/clawdbot-tools.ts | 3 +- src/agents/pi-tools.ts | 3 +- src/agents/tools/agent-step.ts | 2 ++ src/agents/tools/sessions-send-tool.ts | 4 ++- src/agents/tools/sessions-spawn-tool.ts | 3 +- src/gateway/server-methods/agent.ts | 12 +++---- src/gateway/server.agent.test.ts | 45 +++++++++++++++++++++++++ src/utils/message-provider.ts | 35 +++++++++++++++++++ 8 files changed, 97 insertions(+), 10 deletions(-) diff --git a/src/agents/clawdbot-tools.ts b/src/agents/clawdbot-tools.ts index 7ba144ce7..d59e8793f 100644 --- a/src/agents/clawdbot-tools.ts +++ b/src/agents/clawdbot-tools.ts @@ -1,4 +1,5 @@ import type { ClawdbotConfig } from "../config/config.js"; +import type { GatewayMessageProvider } from "../utils/message-provider.js"; import { createAgentsListTool } from "./tools/agents-list-tool.js"; import { createBrowserTool } from "./tools/browser-tool.js"; import { createCanvasTool } from "./tools/canvas-tool.js"; @@ -17,7 +18,7 @@ import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js"; export function createClawdbotTools(options?: { browserControlUrl?: string; agentSessionKey?: string; - agentProvider?: string; + agentProvider?: GatewayMessageProvider; agentAccountId?: string; agentDir?: string; sandboxed?: boolean; diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index c52a38a07..5a475bb1b 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -10,6 +10,7 @@ import { Type } from "@sinclair/typebox"; import type { ClawdbotConfig } from "../config/config.js"; import { detectMime } from "../media/mime.js"; import { isSubagentSessionKey } from "../routing/session-key.js"; +import { resolveGatewayMessageProvider } from "../utils/message-provider.js"; import { startWebLoginWithQr, waitForWebLogin } from "../web/login-qr.js"; import { resolveAgentConfig, @@ -583,7 +584,7 @@ export function createClawdbotCodingTools(options?: { ...createClawdbotTools({ browserControlUrl: sandbox?.browser?.controlUrl, agentSessionKey: options?.sessionKey, - agentProvider: options?.messageProvider, + agentProvider: resolveGatewayMessageProvider(options?.messageProvider), agentAccountId: options?.agentAccountId, agentDir: options?.agentDir, sandboxed: !!sandbox, diff --git a/src/agents/tools/agent-step.ts b/src/agents/tools/agent-step.ts index 84d5fdff8..bcf1609e8 100644 --- a/src/agents/tools/agent-step.ts +++ b/src/agents/tools/agent-step.ts @@ -23,6 +23,7 @@ export async function runAgentStep(params: { message: string; extraSystemPrompt: string; timeoutMs: number; + provider?: string; lane?: string; }): Promise { const stepIdem = crypto.randomUUID(); @@ -33,6 +34,7 @@ export async function runAgentStep(params: { sessionKey: params.sessionKey, idempotencyKey: stepIdem, deliver: false, + provider: params.provider ?? "webchat", lane: params.lane ?? "nested", extraSystemPrompt: params.extraSystemPrompt, }, diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index cce894d44..6e3c285b3 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -10,6 +10,7 @@ import { parseAgentSessionKey, } from "../../routing/session-key.js"; import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js"; +import type { GatewayMessageProvider } from "../../utils/message-provider.js"; import { readLatestAssistantReply, runAgentStep } from "./agent-step.js"; import type { AnyAgentTool } from "./common.js"; import { jsonResult, readStringParam } from "./common.js"; @@ -42,7 +43,7 @@ const SessionsSendToolSchema = Type.Object({ export function createSessionsSendTool(opts?: { agentSessionKey?: string; - agentProvider?: string; + agentProvider?: GatewayMessageProvider; sandboxed?: boolean; }): AnyAgentTool { return { @@ -296,6 +297,7 @@ export function createSessionsSendTool(opts?: { sessionKey: resolvedKey, idempotencyKey, deliver: false, + provider: "webchat", lane: "nested", extraSystemPrompt: agentMessageContext, }; diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index 7680a8afb..204cb72b6 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -9,6 +9,7 @@ import { normalizeAgentId, parseAgentSessionKey, } from "../../routing/session-key.js"; +import type { GatewayMessageProvider } from "../../utils/message-provider.js"; import { resolveAgentConfig } from "../agent-scope.js"; import { buildSubagentSystemPrompt } from "../subagent-announce.js"; import { registerSubagentRun } from "../subagent-registry.js"; @@ -35,7 +36,7 @@ const SessionsSpawnToolSchema = Type.Object({ export function createSessionsSpawnTool(opts?: { agentSessionKey?: string; - agentProvider?: string; + agentProvider?: GatewayMessageProvider; sandboxed?: boolean; }): AnyAgentTool { return { diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 124c59b72..7f0bda404 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -155,13 +155,14 @@ export const agentHandlers: GatewayRequestHandlers = { ? sessionEntry.lastTo.trim() : ""; + const wantsDelivery = request.deliver === true; + const resolvedProvider = (() => { if (requestedProvider === "last") { // WebChat is not a deliverable surface. Treat it as "unset" for routing, // so VoiceWake and CLI callers don't get stuck with deliver=false. - return lastProvider && lastProvider !== "webchat" - ? lastProvider - : "whatsapp"; + if (lastProvider && lastProvider !== "webchat") return lastProvider; + return wantsDelivery ? "whatsapp" : "webchat"; } if ( requestedProvider === "whatsapp" || @@ -173,9 +174,8 @@ export const agentHandlers: GatewayRequestHandlers = { ) { return requestedProvider; } - return lastProvider && lastProvider !== "webchat" - ? lastProvider - : "whatsapp"; + if (lastProvider && lastProvider !== "webchat") return lastProvider; + return wantsDelivery ? "whatsapp" : "webchat"; })(); const resolvedTo = (() => { diff --git a/src/gateway/server.agent.test.ts b/src/gateway/server.agent.test.ts index 6f47d962a..7ef045cbc 100644 --- a/src/gateway/server.agent.test.ts +++ b/src/gateway/server.agent.test.ts @@ -103,11 +103,56 @@ describe("gateway server agent", () => { const call = spy.mock.calls.at(-1)?.[0] as Record; expect(call.sessionKey).toBe("agent:main:subagent:abc"); expect(call.sessionId).toBe("sess-sub"); + expectProviders(call, "webchat"); + expect(call.deliver).toBe(false); + expect(call.to).toBeUndefined(); ws.close(); await server.close(); }); + test("agent falls back to whatsapp when delivery requested and no last provider exists", 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-missing-provider", + updatedAt: Date.now(), + }, + }, + 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-missing-provider", + }); + expect(res.ok).toBe(true); + + const spy = vi.mocked(agentCommand); + const call = spy.mock.calls.at(-1)?.[0] as Record; + expectProviders(call, "whatsapp"); + expect(call.to).toBe("+1555"); + expect(call.deliver).toBe(true); + expect(call.sessionId).toBe("sess-main-missing-provider"); + + ws.close(); + await server.close(); + testState.allowFrom = undefined; + }); + test("agent routes main last-channel whatsapp", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); diff --git a/src/utils/message-provider.ts b/src/utils/message-provider.ts index 6420d53dd..c925e8a7a 100644 --- a/src/utils/message-provider.ts +++ b/src/utils/message-provider.ts @@ -8,6 +8,41 @@ export function normalizeMessageProvider( return normalized; } +export type GatewayMessageProvider = + | "whatsapp" + | "telegram" + | "discord" + | "slack" + | "signal" + | "imessage" + | "msteams" + | "webchat"; + +const GATEWAY_MESSAGE_PROVIDERS: GatewayMessageProvider[] = [ + "whatsapp", + "telegram", + "discord", + "slack", + "signal", + "imessage", + "msteams", + "webchat", +]; + +export function isGatewayMessageProvider( + value: string, +): value is GatewayMessageProvider { + return (GATEWAY_MESSAGE_PROVIDERS as string[]).includes(value); +} + +export function resolveGatewayMessageProvider( + raw?: string | null, +): GatewayMessageProvider | undefined { + const normalized = normalizeMessageProvider(raw); + if (!normalized) return undefined; + return isGatewayMessageProvider(normalized) ? normalized : undefined; +} + export function resolveMessageProvider( primary?: string | null, fallback?: string | null,