fix(gateway): respect explicit voicewake session targets

This commit is contained in:
Longbiao CHEN
2026-04-11 03:12:45 +08:00
committed by Peter Steinberger
parent ef7ad8229a
commit 4cc2ffce09
5 changed files with 57 additions and 39 deletions

View File

@@ -1634,6 +1634,54 @@ describe("gateway agent handler", () => {
});
});
it("does not auto-route voice wake requests with an explicit session key", async () => {
mocks.loadVoiceWakeRoutingConfig.mockResolvedValue({
version: 1,
defaultTarget: { sessionKey: "agent:main:voice" },
routes: [],
updatedAtMs: 0,
});
mocks.resolveVoiceWakeRouteByTrigger.mockReturnValue({ sessionKey: "agent:main:voice" });
mocks.loadSessionEntry.mockImplementation((sessionKey: string) => ({
cfg: {},
storePath: "/tmp/sessions.json",
entry: {
sessionId: "voice-session-id",
updatedAt: Date.now(),
},
canonicalKey: sessionKey,
}));
mocks.updateSessionStore.mockResolvedValue(undefined);
mocks.agentCommand.mockResolvedValue({
payloads: [{ text: "ok" }],
meta: { durationMs: 100 },
});
mocks.loadVoiceWakeRoutingConfig.mockClear();
mocks.resolveVoiceWakeRouteByTrigger.mockClear();
const respond = vi.fn();
await agentHandlers.agent({
params: {
message: "do thing",
sessionKey: "agent:main:research",
voiceWakeTrigger: "robot wake",
idempotencyKey: "test-voice-route-explicit-session",
},
respond,
context: makeContext(),
req: { type: "req", id: "voice-5", method: "agent" },
client: null,
isWebchatConnect: () => false,
});
await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled());
const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { sessionKey?: string };
expect(callArgs.sessionKey).toBe("agent:main:research");
expect(mocks.loadVoiceWakeRoutingConfig).not.toHaveBeenCalled();
expect(mocks.resolveVoiceWakeRouteByTrigger).not.toHaveBeenCalled();
});
it("treats explicit sessionId as an opt-out for voice wake auto-routing", async () => {
mocks.loadVoiceWakeRoutingConfig.mockResolvedValue({
version: 1,
@@ -1657,6 +1705,8 @@ describe("gateway agent handler", () => {
payloads: [{ text: "ok" }],
meta: { durationMs: 100 },
});
mocks.loadVoiceWakeRoutingConfig.mockClear();
mocks.resolveVoiceWakeRouteByTrigger.mockClear();
const respond = vi.fn();
await agentHandlers.agent({
@@ -1669,7 +1719,7 @@ describe("gateway agent handler", () => {
},
respond,
context: makeContext(),
req: { type: "req", id: "voice-5", method: "agent" },
req: { type: "req", id: "voice-6", method: "agent" },
client: null,
isWebchatConnect: () => false,
});
@@ -1681,6 +1731,7 @@ describe("gateway agent handler", () => {
};
expect(callArgs.sessionId).toBe("caller-selected-session-id");
expect(callArgs.sessionKey).toBe("agent:main:main");
expect(mocks.loadVoiceWakeRoutingConfig).not.toHaveBeenCalled();
expect(mocks.resolveVoiceWakeRouteByTrigger).not.toHaveBeenCalled();
});

View File

@@ -113,6 +113,7 @@ import {
waitForTerminalGatewayDedupe,
} from "./agent-wait-dedupe.js";
import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js";
import type { GatewayRequestHandlerOptions, GatewayRequestHandlers } from "./types.js";
const RESET_COMMAND_RE = /^\/(new|reset)(?:\s+([\s\S]*))?$/i;

View File

@@ -62,6 +62,7 @@ describe("createGatewayRequestContext", () => {
markChannelLoggedOut: vi.fn(),
wizardRunner: vi.fn(async () => undefined),
broadcastVoiceWakeChanged: vi.fn(),
broadcastVoiceWakeRoutingChanged: vi.fn(),
unavailableGatewayMethods: new Set(),
});

View File

@@ -56,6 +56,7 @@ export type GatewayRequestContextParams = {
markChannelLoggedOut: GatewayRequestContext["markChannelLoggedOut"];
wizardRunner: GatewayRequestContext["wizardRunner"];
broadcastVoiceWakeChanged: GatewayRequestContext["broadcastVoiceWakeChanged"];
broadcastVoiceWakeRoutingChanged: GatewayRequestContext["broadcastVoiceWakeRoutingChanged"];
unavailableGatewayMethods: ReadonlySet<string>;
};
@@ -149,6 +150,7 @@ export function createGatewayRequestContext(
markChannelLoggedOut: params.markChannelLoggedOut,
wizardRunner: params.wizardRunner,
broadcastVoiceWakeChanged: params.broadcastVoiceWakeChanged,
broadcastVoiceWakeRoutingChanged: params.broadcastVoiceWakeRoutingChanged,
unavailableGatewayMethods: params.unavailableGatewayMethods,
};
}

View File

@@ -27,7 +27,6 @@ import { isTruthyEnvValue, isVitestRuntimeEnv, logAcceptedEnvOption } from "../i
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
import { setGatewaySigusr1RestartPolicy, setPreRestartDeferralCheck } from "../infra/restart.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
import type { VoiceWakeRoutingConfig } from "../infra/voicewake-routing.js";
import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js";
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
@@ -623,41 +622,9 @@ export async function startGatewayServer(
await runClosePrelude();
await createCloseHandler()({ reason: "gateway startup failed" });
};
<<<<<<< HEAD
=======
const nodeRegistry = new NodeRegistry();
const nodePresenceTimers = new Map<string, ReturnType<typeof setInterval>>();
const nodeSubscriptions = createNodeSubscriptionManager();
const sessionEventSubscribers = createSessionEventSubscriberRegistry();
const sessionMessageSubscribers = createSessionMessageSubscriberRegistry();
const nodeSendEvent = (opts: { nodeId: string; event: string; payloadJSON?: string | null }) => {
const payload = safeParseJson(opts.payloadJSON ?? null);
nodeRegistry.sendEvent(opts.nodeId, opts.event, payload);
};
const nodeSendToSession = (sessionKey: string, event: string, payload: unknown) =>
nodeSubscriptions.sendToSession(sessionKey, event, payload, nodeSendEvent);
const nodeSendToAllSubscribed = (event: string, payload: unknown) =>
nodeSubscriptions.sendToAllSubscribed(event, payload, nodeSendEvent);
const nodeSubscribe = nodeSubscriptions.subscribe;
const nodeUnsubscribe = nodeSubscriptions.unsubscribe;
const nodeUnsubscribeAll = nodeSubscriptions.unsubscribeAll;
const broadcastVoiceWakeChanged = (triggers: string[]) => {
broadcast("voicewake.changed", { triggers }, { dropIfSlow: true });
};
const broadcastVoiceWakeRoutingChanged = (config: VoiceWakeRoutingConfig) => {
broadcast("voicewake.routing.changed", { config }, { dropIfSlow: true });
};
const hasMobileNodeConnected = () => hasConnectedMobileNode(nodeRegistry);
applyGatewayLaneConcurrency(cfgAtStart);
let cronState = buildGatewayCronService({
cfg: cfgAtStart,
deps,
broadcast,
});
let { cron, storePath: cronStorePath } = cronState;
deps.cron = cron;
>>>>>>> 85f70db0b2 (feat(voicewake): refresh trigger routing on main)
try {
const earlyRuntime = await startupTrace.measure("runtime.early", () =>
@@ -808,12 +775,8 @@ export async function startGatewayServer(
wizardRunner,
broadcastVoiceWakeChanged,
unavailableGatewayMethods,
<<<<<<< HEAD
});
=======
broadcastVoiceWakeRoutingChanged,
};
>>>>>>> 85f70db0b2 (feat(voicewake): refresh trigger routing on main)
});
setFallbackGatewayContextResolver(() => gatewayRequestContext);