fix: distinguish local vs remote gatewayUrl before suppressing deliveryContext
isRemoteGateway was inferred from gatewayUrl being present, but gatewayUrl overrides are valid for loopback/local targets too (ws://127.0.0.1:<port>, localhost, [::1]). These local-override calls should still forward deliveryContext — treating them as remote falls back to extractDeliveryInfo(sessionKey) and reintroduces the stale heartbeat routing this patch was meant to fix. Fix: export resolveGatewayTarget() from gateway.ts (returns 'local' | 'remote' | undefined) and use it instead of Boolean(gatewayUrl?.trim()). Only gatewayUrl values that classify as 'remote' now suppress deliveryContext. Adds test coverage for the local loopback case.
This commit is contained in:
parent
33c24858ff
commit
e705d9b23d
@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
|
||||
formatDoctorNonInteractiveHint: vi.fn(() => ""),
|
||||
callGatewayTool: vi.fn(async () => ({})),
|
||||
readGatewayCallOptions: vi.fn(() => ({})),
|
||||
resolveGatewayTarget: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/commands.js", () => ({ isRestartEnabled: mocks.isRestartEnabled }));
|
||||
@ -31,6 +32,7 @@ vi.mock("../../infra/restart.js", () => ({
|
||||
vi.mock("./gateway.js", () => ({
|
||||
callGatewayTool: mocks.callGatewayTool,
|
||||
readGatewayCallOptions: mocks.readGatewayCallOptions,
|
||||
resolveGatewayTarget: mocks.resolveGatewayTarget,
|
||||
}));
|
||||
|
||||
import { createGatewayTool } from "./gateway-tool.js";
|
||||
@ -174,6 +176,7 @@ describe("createGatewayTool – live delivery context guard", () => {
|
||||
// destination on the remote host.
|
||||
mocks.callGatewayTool.mockClear();
|
||||
mocks.readGatewayCallOptions.mockReturnValueOnce({ gatewayUrl: "wss://remote-gw.example.com" });
|
||||
mocks.resolveGatewayTarget.mockReturnValueOnce("remote");
|
||||
const tool = createGatewayTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentChannel: "discord",
|
||||
@ -193,6 +196,37 @@ describe("createGatewayTool – live delivery context guard", () => {
|
||||
expect(forwardedParams?.deliveryContext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("forwards live RPC delivery context when gatewayUrl is a local loopback override", async () => {
|
||||
// A gatewayUrl pointing to 127.0.0.1/localhost/[::1] is still the local server;
|
||||
// deliveryContext must be forwarded so restart sentinels use the correct chat destination.
|
||||
mocks.callGatewayTool.mockClear();
|
||||
mocks.readGatewayCallOptions.mockReturnValueOnce({
|
||||
gatewayUrl: "ws://127.0.0.1:18789",
|
||||
});
|
||||
mocks.resolveGatewayTarget.mockReturnValueOnce("local");
|
||||
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: "local loopback patch",
|
||||
gatewayUrl: "ws://127.0.0.1:18789",
|
||||
});
|
||||
|
||||
const forwardedParams = getCallArg<Record<string, unknown>>(mocks.callGatewayTool, 0, 2);
|
||||
expect(forwardedParams?.deliveryContext).toEqual({
|
||||
channel: "discord",
|
||||
to: "123456789",
|
||||
accountId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
|
||||
@ -12,7 +12,7 @@ import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
|
||||
import { callGatewayTool, readGatewayCallOptions, resolveGatewayTarget } from "./gateway.js";
|
||||
|
||||
const log = createSubsystemLogger("gateway-tool");
|
||||
|
||||
@ -274,7 +274,12 @@ export function createGatewayTool(opts?: {
|
||||
// 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());
|
||||
// Only suppress deliveryContext for truly remote gateways. A gatewayUrl
|
||||
// override pointing to a local loopback address (127.0.0.1, localhost,
|
||||
// [::1]) is still the local server and should forward context normally;
|
||||
// treating it as remote would fall back to extractDeliveryInfo(sessionKey)
|
||||
// and reintroduce the stale heartbeat routing this patch was meant to fix.
|
||||
const isRemoteGateway = resolveGatewayTarget(gatewayOpts) === "remote";
|
||||
const deliveryContext =
|
||||
isTargetingOtherSession || isRemoteGateway ? undefined : liveDeliveryContextForRpc;
|
||||
return { sessionKey, note, restartDelayMs, deliveryContext };
|
||||
|
||||
@ -13,7 +13,7 @@ export type GatewayCallOptions = {
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
type GatewayOverrideTarget = "local" | "remote";
|
||||
export type GatewayOverrideTarget = "local" | "remote";
|
||||
|
||||
export function readGatewayCallOptions(params: Record<string, unknown>): GatewayCallOptions {
|
||||
return {
|
||||
@ -113,6 +113,23 @@ function resolveGatewayOverrideToken(params: {
|
||||
}).token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves whether a GatewayCallOptions points to a local or remote gateway.
|
||||
* Returns undefined when no gatewayUrl override is present (default local gateway).
|
||||
* Local loopback overrides (127.0.0.1, localhost, [::1]) return "local";
|
||||
* all other URL overrides return "remote".
|
||||
*/
|
||||
export function resolveGatewayTarget(opts?: GatewayCallOptions): GatewayOverrideTarget | undefined {
|
||||
if (trimToUndefined(opts?.gatewayUrl) === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
return validateGatewayUrlOverrideForAgentTools({
|
||||
cfg,
|
||||
urlOverride: String(opts?.gatewayUrl),
|
||||
}).target;
|
||||
}
|
||||
|
||||
export function resolveGatewayOptions(opts?: GatewayCallOptions) {
|
||||
const cfg = loadConfig();
|
||||
const validatedOverride =
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user