diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 915e546491b..76dfabd8831 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -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(); }); diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 11d99495ea0..dc7d8dabe0a 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -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; diff --git a/src/gateway/server-request-context.test.ts b/src/gateway/server-request-context.test.ts index c4ac81edfad..f2e91b9698e 100644 --- a/src/gateway/server-request-context.test.ts +++ b/src/gateway/server-request-context.test.ts @@ -62,6 +62,7 @@ describe("createGatewayRequestContext", () => { markChannelLoggedOut: vi.fn(), wizardRunner: vi.fn(async () => undefined), broadcastVoiceWakeChanged: vi.fn(), + broadcastVoiceWakeRoutingChanged: vi.fn(), unavailableGatewayMethods: new Set(), }); diff --git a/src/gateway/server-request-context.ts b/src/gateway/server-request-context.ts index 569d116a22d..a799aae6798 100644 --- a/src/gateway/server-request-context.ts +++ b/src/gateway/server-request-context.ts @@ -56,6 +56,7 @@ export type GatewayRequestContextParams = { markChannelLoggedOut: GatewayRequestContext["markChannelLoggedOut"]; wizardRunner: GatewayRequestContext["wizardRunner"]; broadcastVoiceWakeChanged: GatewayRequestContext["broadcastVoiceWakeChanged"]; + broadcastVoiceWakeRoutingChanged: GatewayRequestContext["broadcastVoiceWakeRoutingChanged"]; unavailableGatewayMethods: ReadonlySet; }; @@ -149,6 +150,7 @@ export function createGatewayRequestContext( markChannelLoggedOut: params.markChannelLoggedOut, wizardRunner: params.wizardRunner, broadcastVoiceWakeChanged: params.broadcastVoiceWakeChanged, + broadcastVoiceWakeRoutingChanged: params.broadcastVoiceWakeRoutingChanged, unavailableGatewayMethods: params.unavailableGatewayMethods, }; } diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 8202dedfa49..d8837ca76af 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -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>(); - 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);