From 33c24858ff1d45bba081b24989872706f21b6b37 Mon Sep 17 00:00:00 2001 From: Bryan Marty Date: Mon, 9 Mar 2026 13:42:52 +0000 Subject: [PATCH] fix: omit live deliveryContext when targeting a remote gateway resolveGatewayWriteMeta() was forwarding the local agent run's deliveryContext to config.apply/config.patch/update.run even when gatewayUrl pointed to a remote gateway. Server handlers now prefer params.deliveryContext over extractDeliveryInfo(sessionKey), so a remote restart sentinel would be written with the local chat's channel/ to, causing post-restart wake messages to be delivered to the caller's chat instead of the session that lives on the remote gateway. Fix: gate deliveryContext forwarding on isRemoteGateway (truthy gatewayOpts.gatewayUrl). When targeting a remote gateway, omit deliveryContext so the remote server's extractDeliveryInfo(sessionKey) remains authoritative for the routing of that session. See #18612. --- src/agents/tools/gateway-tool.test.ts | 25 +++++++++++++++++++++++++ src/agents/tools/gateway-tool.ts | 10 +++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/agents/tools/gateway-tool.test.ts b/src/agents/tools/gateway-tool.test.ts index 81ac3b7649f..75039fc53d1 100644 --- a/src/agents/tools/gateway-tool.test.ts +++ b/src/agents/tools/gateway-tool.test.ts @@ -168,6 +168,31 @@ describe("createGatewayTool – live delivery context guard", () => { expect(sentinelPayload?.deliveryContext?.to).toBe("123456789"); }); + it("does not forward live RPC delivery context when gatewayUrl targets a remote gateway", async () => { + // A remote gateway has its own extractDeliveryInfo(sessionKey) — forwarding + // the local agent's deliveryContext would write a sentinel with the wrong + // destination on the remote host. + mocks.callGatewayTool.mockClear(); + mocks.readGatewayCallOptions.mockReturnValueOnce({ gatewayUrl: "wss://remote-gw.example.com" }); + const tool = createGatewayTool({ + agentSessionKey: "agent:main:main", + agentChannel: "discord", + agentTo: "123456789", + }); + + await execTool(tool, { + action: "config.patch", + raw: '{"key":"value"}', + baseHash: "abc123", + sessionKey: "agent:main:main", + note: "remote patch", + gatewayUrl: "wss://remote-gw.example.com", + }); + + const forwardedParams = getCallArg>(mocks.callGatewayTool, 0, 2); + expect(forwardedParams?.deliveryContext).toBeUndefined(); + }); + it("does not forward live RPC delivery context when a non-default agent passes sessionKey='main'", async () => { // "main" resolves to "agent:main:main" (default agent), which differs from the // current session "agent:shopping-claw:main". Live context must NOT be forwarded. diff --git a/src/agents/tools/gateway-tool.ts b/src/agents/tools/gateway-tool.ts index 27c89750764..50e0698b588 100644 --- a/src/agents/tools/gateway-tool.ts +++ b/src/agents/tools/gateway-tool.ts @@ -268,7 +268,15 @@ export function createGatewayTool(opts?: { explicitSessionKey != null && rpcCanonicalizeTarget(explicitSessionKey) !== (rpcOwnKey ? rpcCanonicalizeOwn(rpcOwnKey) : undefined); - const deliveryContext = isTargetingOtherSession ? undefined : liveDeliveryContextForRpc; + // Also omit when the call targets a remote gateway. The remote server's + // extractDeliveryInfo(sessionKey) is the authoritative source for that + // session's delivery route. Forwarding the local agent run's deliveryContext + // would write a sentinel with the wrong chat destination on the remote host, + // causing post-restart wake messages to be sent to the caller's chat instead + // of the session on the remote gateway. See #18612. + const isRemoteGateway = Boolean(gatewayOpts.gatewayUrl?.trim()); + const deliveryContext = + isTargetingOtherSession || isRemoteGateway ? undefined : liveDeliveryContextForRpc; return { sessionKey, note, restartDelayMs, deliveryContext }; };