Gateway: resolve fallback plugin context lazily

This commit is contained in:
scoootscooob 2026-03-22 23:51:30 -07:00
parent 52b92f2973
commit 4580d585ff
3 changed files with 47 additions and 8 deletions

View File

@ -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);
});
});

View File

@ -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<FallbackGatewayContextState>(
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<T>(
},
): Promise<T> {
const scope = getPluginRuntimeGatewayRequestScope();
const context = scope?.context ?? fallbackGatewayContextState.context;
const context = scope?.context ?? getFallbackGatewayContext();
const isWebchatConnect = scope?.isWebchatConnect ?? (() => false);
if (!context) {
throw new Error(

View File

@ -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,