fix(feishu): use full gateway channel runtime

This commit is contained in:
Peter Steinberger
2026-05-30 23:45:52 +01:00
parent 0b0edcdf1c
commit d05e4a4bc6
15 changed files with 85 additions and 107 deletions

View File

@@ -1,5 +1,6 @@
import { EventEmitter } from "node:events";
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { RateLimitError } from "../internal/discord.js";
@@ -65,6 +66,7 @@ function createTestChannelRuntime(): ChannelRuntimeSurface {
},
};
return {
...createPluginRuntimeMock().channel,
runtimeContexts,
};
}

View File

@@ -3704,7 +3704,7 @@ describe("createFeishuMessageReceiveHandler media dedupe", () => {
});
const handler = createFeishuMessageReceiveHandler({
cfg: { channels: { feishu: { dmPolicy: "open" } } } as ClawdbotConfig,
core,
channelRuntime: core.channel,
accountId: "receive-media-dedupe",
chatHistories: new Map(),
handleMessage,

View File

@@ -440,6 +440,7 @@ export async function handleFeishuMessage(params: {
botOpenId?: string;
botName?: string;
runtime?: RuntimeEnv;
channelRuntime?: ReturnType<typeof getFeishuRuntime>["channel"];
chatHistories?: Map<string, HistoryEntry[]>;
accountId?: string;
processingClaimHeld?: boolean;
@@ -450,6 +451,7 @@ export async function handleFeishuMessage(params: {
botOpenId,
botName,
runtime,
channelRuntime,
chatHistories,
accountId,
processingClaimHeld = false,
@@ -709,7 +711,9 @@ export async function handleFeishuMessage(params: {
}
try {
const core = getFeishuRuntime();
const core = {
channel: channelRuntime ?? getFeishuRuntime().channel,
} as ReturnType<typeof getFeishuRuntime>;
const pairing = createChannelPairingController({
core,
channel: "feishu",

View File

@@ -3,7 +3,7 @@ import {
isFutureDateTimestampMs,
resolveExpiresAtMsFromDurationMs,
} from "openclaw/plugin-sdk/number-runtime";
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
import { resolveFeishuRuntimeAccount } from "./accounts.js";
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
import { decodeFeishuCardAction, buildFeishuCardActionTextFallback } from "./card-interaction.js";
@@ -170,6 +170,7 @@ async function dispatchSyntheticCommand(params: {
account: ReturnType<typeof resolveFeishuRuntimeAccount>;
botOpenId?: string;
runtime?: RuntimeEnv;
channelRuntime?: PluginRuntime["channel"];
accountId?: string;
chatType?: "p2p" | "group";
}): Promise<void> {
@@ -184,6 +185,7 @@ async function dispatchSyntheticCommand(params: {
event: buildSyntheticMessageEvent(params.event, params.command, resolvedChatType),
botOpenId: params.botOpenId,
runtime: params.runtime,
channelRuntime: params.channelRuntime,
accountId: params.accountId,
});
}
@@ -342,6 +344,7 @@ export async function handleFeishuCardAction(params: {
event: FeishuCardActionEvent;
botOpenId?: string;
runtime?: RuntimeEnv;
channelRuntime?: PluginRuntime["channel"];
accountId?: string;
}): Promise<void> {
const { cfg, event, runtime, accountId } = params;
@@ -465,6 +468,7 @@ export async function handleFeishuCardAction(params: {
account,
botOpenId: params.botOpenId,
runtime,
channelRuntime: params.channelRuntime,
accountId,
chatType: envelope.c?.t,
});
@@ -495,6 +499,7 @@ export async function handleFeishuCardAction(params: {
account,
botOpenId: params.botOpenId,
runtime,
channelRuntime: params.channelRuntime,
accountId,
});
completeFeishuCardActionToken({ token: event.token, accountId: account.accountId });

View File

@@ -31,6 +31,7 @@ import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { PluginRuntime } from "../runtime-api.js";
import {
inspectFeishuCredentials,
listEnabledFeishuAccounts,
@@ -1319,6 +1320,9 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
return monitorFeishuProvider({
config: ctx.cfg,
runtime: ctx.runtime,
// Gateway provides the full channel runtime here; the public SDK type
// stays context-only for external compatibility.
channelRuntime: ctx.channelRuntime as PluginRuntime["channel"] | undefined,
abortSignal: ctx.abortSignal,
accountId: ctx.accountId,
});

View File

@@ -1,6 +1,6 @@
import * as crypto from "node:crypto";
import type * as Lark from "@larksuiteoapi/node-sdk";
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "../runtime-api.js";
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv, HistoryEntry } from "../runtime-api.js";
import { raceWithTimeoutAndAbort } from "./async.js";
import {
handleFeishuMessage,
@@ -164,6 +164,7 @@ function normalizeFeishuChatType(value: unknown): FeishuChatType | undefined {
type RegisterEventHandlersContext = {
cfg: ClawdbotConfig;
accountId: string;
channelRuntime: PluginRuntime["channel"];
runtime?: RuntimeEnv;
chatHistories: Map<string, HistoryEntry[]>;
fireAndForget?: boolean;
@@ -266,7 +267,7 @@ function registerEventHandlers(
eventDispatcher: Lark.EventDispatcher,
context: RegisterEventHandlersContext,
): void {
const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
const { cfg, accountId, channelRuntime, runtime, chatHistories, fireAndForget } = context;
const log = runtime?.log ?? console.log;
const error = runtime?.error ?? console.error;
const runFeishuHandler = async (params: { task: () => Promise<void>; errorMessage: string }) => {
@@ -286,7 +287,7 @@ function registerEventHandlers(
eventDispatcher.register({
"im.message.receive_v1": createFeishuMessageReceiveHandler({
cfg,
core: getFeishuRuntime(),
channelRuntime,
accountId,
runtime,
chatHistories,
@@ -353,6 +354,7 @@ function registerEventHandlers(
botOpenId: myBotId,
botName: botNames.get(accountId),
runtime,
channelRuntime,
chatHistories,
accountId,
});
@@ -383,6 +385,7 @@ function registerEventHandlers(
botOpenId: myBotId,
botName: botNames.get(accountId),
runtime,
channelRuntime,
chatHistories,
accountId,
});
@@ -396,6 +399,7 @@ function registerEventHandlers(
runtime,
chatHistories,
fireAndForget,
channelRuntime,
}),
"card.action.trigger": async (data: unknown) => {
try {
@@ -409,6 +413,7 @@ function registerEventHandlers(
event,
botOpenId: botOpenIds.get(accountId),
runtime,
channelRuntime,
accountId,
});
if (fireAndForget) {
@@ -432,6 +437,7 @@ export type BotOpenIdSource =
export type MonitorSingleAccountParams = {
cfg: ClawdbotConfig;
account: ResolvedFeishuAccount;
channelRuntime?: PluginRuntime["channel"];
runtime?: RuntimeEnv;
abortSignal?: AbortSignal;
botOpenIdSource?: BotOpenIdSource;
@@ -473,10 +479,12 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
const eventDispatcher = createEventDispatcher(account);
const chatHistories = new Map<string, HistoryEntry[]>();
threadBindingManager = createFeishuThreadBindingManager({ accountId, cfg });
const channelRuntime = params.channelRuntime ?? getFeishuRuntime().channel;
registerEventHandlers(eventDispatcher, {
cfg,
accountId,
channelRuntime,
runtime,
chatHistories,
fireAndForget: params.fireAndForget ?? true,

View File

@@ -1,5 +1,5 @@
import { isRecord, readStringValue as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ClawdbotConfig, HistoryEntry, RuntimeEnv } from "../runtime-api.js";
import type { ClawdbotConfig, HistoryEntry, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
import { maybeHandleFeishuQuickActionMenu } from "./card-ux-launcher.js";
import {
@@ -54,6 +54,7 @@ export function createFeishuBotMenuHandler(params: {
cfg: ClawdbotConfig;
accountId: string;
runtime?: RuntimeEnv;
channelRuntime?: PluginRuntime["channel"];
chatHistories: Map<string, HistoryEntry[]>;
fireAndForget?: boolean;
getBotOpenId?: (accountId: string) => string | undefined;
@@ -117,6 +118,7 @@ export function createFeishuBotMenuHandler(params: {
botOpenId: getBotOpenId(accountId),
botName: getBotName(accountId),
runtime,
channelRuntime: params.channelRuntime,
chatHistories,
accountId,
processingClaimHeld: true,

View File

@@ -12,7 +12,7 @@ import type { FeishuChatType } from "./types.js";
type FeishuMessageReceiveHandlerContext = {
cfg: ClawdbotConfig;
core: PluginRuntime;
channelRuntime: PluginRuntime["channel"];
accountId: string;
runtime?: RuntimeEnv;
chatHistories: Map<string, HistoryEntry[]>;
@@ -23,6 +23,7 @@ type FeishuMessageReceiveHandlerContext = {
botOpenId?: string;
botName?: string;
runtime?: RuntimeEnv;
channelRuntime?: PluginRuntime["channel"];
chatHistories?: Map<string, HistoryEntry[]>;
accountId?: string;
processingClaimHeld?: boolean;
@@ -154,7 +155,7 @@ function resolveFeishuDebounceMentions(params: {
export function createFeishuMessageReceiveHandler({
cfg,
core,
channelRuntime,
accountId,
runtime,
chatHistories,
@@ -168,7 +169,7 @@ export function createFeishuMessageReceiveHandler({
resolveSequentialKey = ({ accountId, event }) =>
`feishu:${accountId}:${event.message.chat_id?.trim() || "unknown"}`,
}: FeishuMessageReceiveHandlerContext): (data: unknown) => Promise<void> {
const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
const inboundDebounceMs = channelRuntime.debounce.resolveInboundDebounceMs({
cfg,
channel: "feishu",
});
@@ -196,6 +197,7 @@ export function createFeishuMessageReceiveHandler({
botOpenId: getBotOpenId(accountId),
botName: getBotName(accountId),
runtime,
channelRuntime,
chatHistories,
accountId,
processingClaimHeld: true,
@@ -238,7 +240,7 @@ export function createFeishuMessageReceiveHandler({
}
};
const inboundDebouncer = core.channel.debounce.createInboundDebouncer<FeishuMessageEvent>({
const inboundDebouncer = channelRuntime.debounce.createInboundDebouncer<FeishuMessageEvent>({
debounceMs: inboundDebounceMs,
buildKey: (event) => {
const chatId = event.message.chat_id?.trim();
@@ -255,7 +257,7 @@ export function createFeishuMessageReceiveHandler({
return false;
}
const text = resolveDebounceText(event);
return Boolean(text) && !core.channel.commands.isControlCommandMessage(text, cfg);
return Boolean(text) && !channelRuntime.commands.isControlCommandMessage(text, cfg);
},
onFlush: async (entries) => {
const last = entries.at(-1);

View File

@@ -1,4 +1,4 @@
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
import { listEnabledFeishuAccounts, resolveFeishuRuntimeAccount } from "./accounts.js";
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
import {
@@ -11,6 +11,7 @@ import {
export type MonitorFeishuOpts = {
config?: ClawdbotConfig;
runtime?: RuntimeEnv;
channelRuntime?: PluginRuntime["channel"];
abortSignal?: AbortSignal;
accountId?: string;
};
@@ -48,6 +49,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
return monitorSingleAccount({
cfg,
account,
channelRuntime: opts.channelRuntime,
runtime: opts.runtime,
abortSignal: opts.abortSignal,
});
@@ -85,6 +87,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
monitorSingleAccount({
cfg,
account,
channelRuntime: opts.channelRuntime,
runtime: opts.runtime,
abortSignal: opts.abortSignal,
botOpenIdSource: { kind: "prefetched", botOpenId, botName },

View File

@@ -32,11 +32,10 @@ export type ChannelRuntimeContextRegistry = {
};
/**
* Minimal channel-runtime surface threaded through gateway/setup flows.
* Minimal channel-runtime surface exported through the public plugin SDK.
*
* Most callers only pass this object through or use `runtimeContexts`.
* Keeping this leaf contract small avoids dragging the full plugin runtime
* graph into generic channel adapter types.
* Gateway startup supplies the full plugin channel runtime, but external callers
* may still type context-only helpers against this compatibility surface.
*/
export type ChannelRuntimeSurface = {
runtimeContexts: ChannelRuntimeContextRegistry;

View File

@@ -248,9 +248,8 @@ export type ChannelGatewayContext<ResolvedAccount = unknown> = {
/**
* Optional channel runtime helpers for external channel plugins.
*
* This field provides access to advanced Plugin SDK features that are
* available to external plugins but not to built-in channels (which can
* directly import internal modules).
* This field provides the canonical channel runtime helpers for channel
* dispatch, routing, session, reply, and startup context work.
*
* ## Available Features
*
@@ -265,7 +264,7 @@ export type ChannelGatewayContext<ResolvedAccount = unknown> = {
*
* ## Use Cases
*
* External channel plugins (e.g., email, SMS, custom integrations) that need:
* Channel plugins that need:
* - AI-powered response generation and delivery
* - Advanced text processing and formatting
* - Session tracking and management
@@ -299,13 +298,9 @@ export type ChannelGatewayContext<ResolvedAccount = unknown> = {
* ## Backward Compatibility
*
* - This field is **optional** - channels that don't need it can ignore it
* - Bundled channels typically don't use this field
* because they can directly import internal modules
* - Gateway startup passes a full `createPluginRuntime().channel` surface
* when a runtime resolver is configured
* - External plugins should check for undefined before using
* - `runtimeContexts` is the stable startup-safe subset. Bundled channels
* may receive only that subset during provider boot.
* - External channel plugins that need reply/routing/session helpers receive
* a full `createPluginRuntime().channel` surface from the Gateway.
*
* @since Plugin SDK 2026.2.19
* @see {@link https://docs.openclaw.ai/plugins/building-plugins | Plugin SDK documentation}

View File

@@ -1,5 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
import {
type ChannelGatewayContext,
type ChannelId,
@@ -12,7 +11,6 @@ import {
} from "../logging/subsystem.js";
import { createEmptyPluginRegistry, type PluginRegistry } from "../plugins/registry.js";
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
import { createChannelRuntimeContextRegistry } from "../plugins/runtime/channel-runtime-contexts.js";
import { createRuntimeChannel } from "../plugins/runtime/runtime-channel.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
@@ -159,9 +157,8 @@ function installTestRegistry(
}
function createManager(options?: {
channelRuntime?: ChannelRuntimeSurface;
resolveChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
resolveStartupChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
channelRuntime?: PluginRuntime["channel"];
resolveChannelRuntime?: () => PluginRuntime["channel"] | Promise<PluginRuntime["channel"]>;
getRuntimeConfig?: () => Record<string, unknown>;
channelIds?: ChannelId[];
startupTrace?: { measure: <T>(name: string, run: () => T | Promise<T>) => Promise<T> };
@@ -186,9 +183,6 @@ function createManager(options?: {
...(options?.resolveChannelRuntime
? { resolveChannelRuntime: options.resolveChannelRuntime }
: {}),
...(options?.resolveStartupChannelRuntime
? { resolveStartupChannelRuntime: options.resolveStartupChannelRuntime }
: {}),
...(options?.startupTrace ? { startupTrace: options.startupTrace } : {}),
});
createdManagers.push({ channelIds, manager });
@@ -785,32 +779,31 @@ describe("server-channels auto restart", () => {
expect(ctx?.channelRuntime).not.toBe(channelRuntime);
});
it("uses a lightweight startup runtime for bundled channels", async () => {
it("passes the full runtime path to bundled channel startup", async () => {
const fullRuntime = {
...createRuntimeChannel(),
marker: "full-channel-runtime",
} as PluginRuntime["channel"] & { marker: string };
const startupRuntime = {
runtimeContexts: createChannelRuntimeContextRegistry(),
marker: "startup-channel-runtime",
};
const resolveChannelRuntime = vi.fn(() => fullRuntime);
const resolveStartupChannelRuntime = vi.fn(() => startupRuntime);
const startAccount = vi.fn(async (_ctx: ChannelGatewayContext<TestAccount>) => {});
installTestRegistry({ plugin: createTestPlugin({ startAccount }), origin: "bundled" });
const manager = createManager({ resolveChannelRuntime, resolveStartupChannelRuntime });
installTestRegistry({
plugin: createTestPlugin({ startAccount }),
origin: "bundled",
});
const manager = createManager({ resolveChannelRuntime });
await manager.startChannels();
expect(resolveStartupChannelRuntime).toHaveBeenCalledTimes(1);
expect(resolveChannelRuntime).not.toHaveBeenCalled();
expect(resolveChannelRuntime).toHaveBeenCalledTimes(1);
expect(startAccount).toHaveBeenCalledTimes(1);
const ctx = firstStartAccountContext(startAccount);
expect((ctx?.channelRuntime as { marker?: string } | undefined)?.marker).toBe(
"startup-channel-runtime",
"full-channel-runtime",
);
expect(typeof (ctx?.channelRuntime as PluginRuntime["channel"] | undefined)?.inbound.run).toBe(
"function",
);
expect(ctx?.channelRuntime).not.toBe(startupRuntime);
});
it("keeps the full runtime path for non-bundled channels", async () => {
@@ -818,20 +811,14 @@ describe("server-channels auto restart", () => {
...createRuntimeChannel(),
marker: "full-channel-runtime",
} as PluginRuntime["channel"] & { marker: string };
const startupRuntime = {
runtimeContexts: createChannelRuntimeContextRegistry(),
marker: "startup-channel-runtime",
};
const resolveChannelRuntime = vi.fn(() => fullRuntime);
const resolveStartupChannelRuntime = vi.fn(() => startupRuntime);
const startAccount = vi.fn(async (_ctx: ChannelGatewayContext<TestAccount>) => {});
installTestRegistry({ plugin: createTestPlugin({ startAccount }), origin: "workspace" });
const manager = createManager({ resolveChannelRuntime, resolveStartupChannelRuntime });
const manager = createManager({ resolveChannelRuntime });
await manager.startChannels();
expect(resolveStartupChannelRuntime).not.toHaveBeenCalled();
expect(resolveChannelRuntime).toHaveBeenCalledTimes(1);
const ctx = firstStartAccountContext(startAccount);
expect((ctx?.channelRuntime as { marker?: string } | undefined)?.marker).toBe(

View File

@@ -1,11 +1,5 @@
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
import {
type ChannelId,
getChannelPlugin,
getLoadedChannelPluginOrigin,
listChannelPlugins,
} from "../channels/plugins/index.js";
import { type ChannelId, getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
import type { ChannelAccountSnapshot } from "../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { startChannelApprovalHandlerBootstrap } from "../infra/approval-handler-bootstrap.js";
@@ -20,6 +14,7 @@ import {
} from "../logging/subsystem.js";
import { withPluginHttpRouteRegistry } from "../plugins/http-registry.js";
import type { PluginRegistry } from "../plugins/registry.js";
import type { PluginRuntimeChannel } from "../plugins/runtime/types-channel.js";
import { resolveAccountEntry, resolveNormalizedAccountEntry } from "../routing/account-lookup.js";
import {
DEFAULT_ACCOUNT_ID,
@@ -144,15 +139,12 @@ type ChannelManagerOptions = {
channelLogs: Partial<Record<ChannelId, SubsystemLogger>>;
channelRuntimeEnvs: Partial<Record<ChannelId, RuntimeEnv>>;
/**
* Optional channel runtime helpers for external channel plugins.
* Optional channel runtime helpers for channel plugins.
*
* When provided, this value is passed to all channel plugins via the
* `channelRuntime` field in `ChannelGatewayContext`, enabling external
* plugins to access advanced Plugin SDK features (AI dispatch, routing,
* text processing, etc.).
*
* Bundled channels typically don't use this because they can directly
* import internal modules from the monorepo.
* plugins to access Plugin SDK channel features (AI dispatch, routing,
* session management, startup runtime contexts, text processing, etc.).
*
* This field is optional - omitting it maintains backward compatibility
* with existing channels. When provided, it must be a real
@@ -173,22 +165,16 @@ type ChannelManagerOptions = {
* @since Plugin SDK 2026.2.19
* @see {@link ChannelGatewayContext.channelRuntime}
*/
channelRuntime?: ChannelRuntimeSurface;
channelRuntime?: PluginRuntimeChannel;
/**
* Lazily resolves optional channel runtime helpers for external channel plugins.
* Lazily resolves optional channel runtime helpers for channel plugins.
*
* Use this when the caller wants to avoid instantiating the full plugin channel
* runtime during gateway startup. The manager only needs the runtime surface once
* a channel account actually starts. The resolved value must be a real
* `createPluginRuntime().channel` surface.
*/
resolveChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
/**
* Lightweight channel runtime used for bundled channel startup. Bundled
* channels only need `runtimeContexts` while booting, so this avoids pulling
* the full reply/routing/session runtime graph onto the critical path.
*/
resolveStartupChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
resolveChannelRuntime?: () => PluginRuntimeChannel | Promise<PluginRuntimeChannel>;
getPluginHttpRouteRegistry?: () => PluginRegistry;
startupTrace?: GatewayStartupTrace;
deferStartupAccountStartsUntil?: Promise<void>;
@@ -238,7 +224,6 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
channelRuntimeEnvs,
channelRuntime,
resolveChannelRuntime,
resolveStartupChannelRuntime,
getPluginHttpRouteRegistry,
startupTrace,
} = opts;
@@ -347,18 +332,10 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
return next;
};
const getChannelRuntime = async (
channelId: ChannelId,
): Promise<ChannelRuntimeSurface | undefined> => {
const getChannelRuntime = async (): Promise<PluginRuntimeChannel | undefined> => {
if (channelRuntime) {
return channelRuntime;
}
if (getLoadedChannelPluginOrigin(channelId) === "bundled") {
const startupRuntime = await resolveStartupChannelRuntime?.();
if (startupRuntime) {
return startupRuntime;
}
}
return await resolveChannelRuntime?.();
};
const measureStartup = async <T>(name: string, run: () => T | Promise<T>): Promise<T> => {
@@ -449,8 +426,11 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
let handedOffTask = false;
const log = ensureChannelLog(channelId);
const runtime = ensureChannelRuntime(channelId);
let scopedChannelRuntime: ReturnType<typeof createTaskScopedChannelRuntime> | null = null;
let channelRuntimeForTask: ChannelRuntimeSurface | undefined;
let scopedChannelRuntime: {
channelRuntime?: PluginRuntimeChannel;
dispose: () => void;
} | null = null;
let channelRuntimeForTask: PluginRuntimeChannel | undefined;
let stopApprovalBootstrap: () => Promise<void> = async () => {};
const stopTaskScopedApprovalRuntime = async () => {
const scopedRuntime = scopedChannelRuntime;
@@ -519,7 +499,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
scopedChannelRuntime = await measureStartup(`channels.${channelId}.runtime`, async () =>
createTaskScopedChannelRuntime({
channelRuntime: await getChannelRuntime(channelId),
channelRuntime: await getChannelRuntime(),
}),
);
channelRuntimeForTask = scopedChannelRuntime.channelRuntime;

View File

@@ -1,7 +1,6 @@
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
import { getActiveEmbeddedRunCount } from "../agents/embedded-agent-runner/run-state.js";
import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js";
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
import {
getLoadedChannelPluginEntryById,
listLoadedChannelPlugins,
@@ -177,7 +176,6 @@ const logTailscale = log.child("tailscale");
const logChannels = log.child("channels");
let cachedChannelRuntimePromise: Promise<PluginRuntime["channel"]> | null = null;
let cachedStartupChannelRuntimePromise: Promise<ChannelRuntimeSurface> | null = null;
function getChannelRuntime() {
cachedChannelRuntimePromise ??= import("../plugins/runtime/runtime-channel.js").then(
@@ -186,16 +184,6 @@ function getChannelRuntime() {
return cachedChannelRuntimePromise;
}
function getStartupChannelRuntime() {
cachedStartupChannelRuntimePromise ??=
import("../plugins/runtime/channel-runtime-contexts.js").then(
({ createChannelRuntimeContextRegistry }) => ({
runtimeContexts: createChannelRuntimeContextRegistry(),
}),
);
return cachedStartupChannelRuntimePromise;
}
async function closeMcpLoopbackServerOnDemand(): Promise<void> {
const { closeMcpLoopbackServer } = await import("./mcp-http.js");
await closeMcpLoopbackServer();
@@ -864,7 +852,6 @@ export async function startGatewayServer(
channelLogs,
channelRuntimeEnvs,
resolveChannelRuntime: getChannelRuntime,
resolveStartupChannelRuntime: getStartupChannelRuntime,
getPluginHttpRouteRegistry: () => pluginRegistry,
startupTrace,
deferStartupAccountStartsUntil: startupAccountStartsReady,

View File

@@ -83,10 +83,10 @@ export function watchChannelRuntimeContexts(
});
}
export function createTaskScopedChannelRuntime(params: {
channelRuntime?: ChannelRuntimeSurface;
export function createTaskScopedChannelRuntime<T extends ChannelRuntimeSurface>(params: {
channelRuntime?: T;
}): {
channelRuntime?: ChannelRuntimeSurface;
channelRuntime?: T;
dispose: () => void;
} {
const baseRuntime = params.channelRuntime;
@@ -114,7 +114,7 @@ export function createTaskScopedChannelRuntime(params: {
};
};
const scopedRuntime: ChannelRuntimeSurface = {
const scopedRuntime = {
...baseRuntime,
runtimeContexts: {
...runtimeContexts,
@@ -123,7 +123,7 @@ export function createTaskScopedChannelRuntime(params: {
return trackLease(lease);
},
},
};
} as T;
return {
channelRuntime: scopedRuntime,