diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index cb345ee7894..a7de207cef5 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -616,4 +616,31 @@ describe("loadGatewayPlugins", () => { | undefined; expect(dispatched?.marker).toBe("after-mutation"); }); + + test("resolves fallback context lazily when a resolver is registered", async () => { + const serverPlugins = serverPluginsModule; + const runtime = await createSubagentRuntime(serverPlugins); + let currentContext = createTestContext("before-resolver-update"); + + serverPlugins.setFallbackGatewayContextResolver(() => currentContext); + await runtime.run({ sessionKey: "s-4", message: "before resolver update" }); + expect(getLastDispatchedContext()).toBe(currentContext); + + currentContext = createTestContext("after-resolver-update"); + await runtime.run({ sessionKey: "s-4", message: "after resolver update" }); + expect(getLastDispatchedContext()).toBe(currentContext); + }); + + test("prefers resolver output over an older fallback context snapshot", async () => { + const serverPlugins = serverPluginsModule; + const runtime = await createSubagentRuntime(serverPlugins); + const staleContext = createTestContext("stale-snapshot"); + const freshContext = createTestContext("fresh-resolver"); + + serverPlugins.setFallbackGatewayContext(staleContext); + serverPlugins.setFallbackGatewayContextResolver(() => freshContext); + + await runtime.run({ sessionKey: "s-5", message: "prefer resolver" }); + expect(getLastDispatchedContext()).toBe(freshContext); + }); }); diff --git a/src/gateway/server-plugins.ts b/src/gateway/server-plugins.ts index 6f72e5c67c7..3611d834610 100644 --- a/src/gateway/server-plugins.ts +++ b/src/gateway/server-plugins.ts @@ -32,16 +32,28 @@ const FALLBACK_GATEWAY_CONTEXT_STATE_KEY: unique symbol = Symbol.for( type FallbackGatewayContextState = { context: GatewayRequestContext | undefined; + resolveContext: (() => GatewayRequestContext | undefined) | undefined; }; const fallbackGatewayContextState = resolveGlobalSingleton( FALLBACK_GATEWAY_CONTEXT_STATE_KEY, - () => ({ context: undefined }), + () => ({ context: undefined, resolveContext: undefined }), ); export function setFallbackGatewayContext(ctx: GatewayRequestContext): void { - // TODO: This startup snapshot can become stale if runtime config/context changes. fallbackGatewayContextState.context = ctx; + fallbackGatewayContextState.resolveContext = undefined; +} + +export function setFallbackGatewayContextResolver( + resolveContext: () => GatewayRequestContext | undefined, +): void { + fallbackGatewayContextState.resolveContext = resolveContext; +} + +function getFallbackGatewayContext(): GatewayRequestContext | undefined { + const resolved = fallbackGatewayContextState.resolveContext?.(); + return resolved ?? fallbackGatewayContextState.context; } type PluginSubagentOverridePolicy = { @@ -238,7 +250,7 @@ async function dispatchGatewayMethod( }, ): Promise { const scope = getPluginRuntimeGatewayRequestScope(); - const context = scope?.context ?? fallbackGatewayContextState.context; + const context = scope?.context ?? getFallbackGatewayContext(); const isWebchatConnect = scope?.isWebchatConnect ?? (() => false); if (!context) { throw new Error( diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 20dfe75dc50..b1e249271b7 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -103,7 +103,7 @@ import { createSecretsHandlers } from "./server-methods/secrets.js"; import { hasConnectedMobileNode } from "./server-mobile-nodes.js"; import { loadGatewayModelCatalog } from "./server-model-catalog.js"; import { createNodeSubscriptionManager } from "./server-node-subscriptions.js"; -import { loadGatewayPlugins, setFallbackGatewayContext } from "./server-plugins.js"; +import { loadGatewayPlugins, setFallbackGatewayContextResolver } from "./server-plugins.js"; import { createGatewayReloadHandlers } from "./server-reload-handlers.js"; import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js"; import { createGatewayRuntimeState } from "./server-runtime-state.js"; @@ -1126,10 +1126,10 @@ export async function startGatewayServer( broadcastVoiceWakeChanged, }; - // Store the gateway context as a fallback for plugin subagent dispatch - // in non-WS paths (Telegram polling, WhatsApp, etc.) where no per-request - // scope is set via AsyncLocalStorage. - setFallbackGatewayContext(gatewayRequestContext); + // Register a lazy fallback for plugin subagent dispatch in non-WS paths + // (Telegram polling, WhatsApp, etc.) so later runtime swaps can expose the + // current gateway context without relying on a startup snapshot. + setFallbackGatewayContextResolver(() => gatewayRequestContext); attachGatewayWsHandlers({ wss,