diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index 906eae2d81a..89f9ac1e374 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -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, }; } diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index a35bae56bbb..c76b36dd68a 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -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, diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index c353f1dee6b..45253f3f95c 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -440,6 +440,7 @@ export async function handleFeishuMessage(params: { botOpenId?: string; botName?: string; runtime?: RuntimeEnv; + channelRuntime?: ReturnType["channel"]; chatHistories?: Map; 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; const pairing = createChannelPairingController({ core, channel: "feishu", diff --git a/extensions/feishu/src/card-action.ts b/extensions/feishu/src/card-action.ts index 7a88729bf2f..99ff832e026 100644 --- a/extensions/feishu/src/card-action.ts +++ b/extensions/feishu/src/card-action.ts @@ -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; botOpenId?: string; runtime?: RuntimeEnv; + channelRuntime?: PluginRuntime["channel"]; accountId?: string; chatType?: "p2p" | "group"; }): Promise { @@ -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 { 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 }); diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 6a947fd97fa..fa9a51c760d 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -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; 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; 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(); threadBindingManager = createFeishuThreadBindingManager({ accountId, cfg }); + const channelRuntime = params.channelRuntime ?? getFeishuRuntime().channel; registerEventHandlers(eventDispatcher, { cfg, accountId, + channelRuntime, runtime, chatHistories, fireAndForget: params.fireAndForget ?? true, diff --git a/extensions/feishu/src/monitor.bot-menu-handler.ts b/extensions/feishu/src/monitor.bot-menu-handler.ts index 0d6b4144d17..2a79fc19510 100644 --- a/extensions/feishu/src/monitor.bot-menu-handler.ts +++ b/extensions/feishu/src/monitor.bot-menu-handler.ts @@ -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; 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, diff --git a/extensions/feishu/src/monitor.message-handler.ts b/extensions/feishu/src/monitor.message-handler.ts index 0342663c383..8eb36d80d7c 100644 --- a/extensions/feishu/src/monitor.message-handler.ts +++ b/extensions/feishu/src/monitor.message-handler.ts @@ -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; @@ -23,6 +23,7 @@ type FeishuMessageReceiveHandlerContext = { botOpenId?: string; botName?: string; runtime?: RuntimeEnv; + channelRuntime?: PluginRuntime["channel"]; chatHistories?: Map; 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 { - 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({ + const inboundDebouncer = channelRuntime.debounce.createInboundDebouncer({ 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); diff --git a/extensions/feishu/src/monitor.ts b/extensions/feishu/src/monitor.ts index 5110a313b37..f75839c7565 100644 --- a/extensions/feishu/src/monitor.ts +++ b/extensions/feishu/src/monitor.ts @@ -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 }, diff --git a/src/channels/plugins/channel-runtime-surface.types.ts b/src/channels/plugins/channel-runtime-surface.types.ts index b06eab1be1a..130e4106bcd 100644 --- a/src/channels/plugins/channel-runtime-surface.types.ts +++ b/src/channels/plugins/channel-runtime-surface.types.ts @@ -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; diff --git a/src/channels/plugins/types.adapters.ts b/src/channels/plugins/types.adapters.ts index 021fc788b92..7b0c9ccf98e 100644 --- a/src/channels/plugins/types.adapters.ts +++ b/src/channels/plugins/types.adapters.ts @@ -248,9 +248,8 @@ export type ChannelGatewayContext = { /** * 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 = { * * ## 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 = { * ## 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} diff --git a/src/gateway/server-channels.test.ts b/src/gateway/server-channels.test.ts index f588a6739e8..477c7e50842 100644 --- a/src/gateway/server-channels.test.ts +++ b/src/gateway/server-channels.test.ts @@ -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; - resolveStartupChannelRuntime?: () => ChannelRuntimeSurface | Promise; + channelRuntime?: PluginRuntime["channel"]; + resolveChannelRuntime?: () => PluginRuntime["channel"] | Promise; getRuntimeConfig?: () => Record; channelIds?: ChannelId[]; startupTrace?: { measure: (name: string, run: () => T | Promise) => Promise }; @@ -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) => {}); - 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) => {}); 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( diff --git a/src/gateway/server-channels.ts b/src/gateway/server-channels.ts index a9af04d3bc0..d6036c949d3 100644 --- a/src/gateway/server-channels.ts +++ b/src/gateway/server-channels.ts @@ -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>; channelRuntimeEnvs: Partial>; /** - * 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; - /** - * 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; + resolveChannelRuntime?: () => PluginRuntimeChannel | Promise; getPluginHttpRouteRegistry?: () => PluginRegistry; startupTrace?: GatewayStartupTrace; deferStartupAccountStartsUntil?: Promise; @@ -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 => { + const getChannelRuntime = async (): Promise => { if (channelRuntime) { return channelRuntime; } - if (getLoadedChannelPluginOrigin(channelId) === "bundled") { - const startupRuntime = await resolveStartupChannelRuntime?.(); - if (startupRuntime) { - return startupRuntime; - } - } return await resolveChannelRuntime?.(); }; const measureStartup = async (name: string, run: () => T | Promise): Promise => { @@ -449,8 +426,11 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage let handedOffTask = false; const log = ensureChannelLog(channelId); const runtime = ensureChannelRuntime(channelId); - let scopedChannelRuntime: ReturnType | null = null; - let channelRuntimeForTask: ChannelRuntimeSurface | undefined; + let scopedChannelRuntime: { + channelRuntime?: PluginRuntimeChannel; + dispose: () => void; + } | null = null; + let channelRuntimeForTask: PluginRuntimeChannel | undefined; let stopApprovalBootstrap: () => Promise = 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; diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 0a525d2c4c1..3f232847329 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -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 | null = null; -let cachedStartupChannelRuntimePromise: Promise | 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 { 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, diff --git a/src/infra/channel-runtime-context.ts b/src/infra/channel-runtime-context.ts index da79fcef539..0d1c63645d5 100644 --- a/src/infra/channel-runtime-context.ts +++ b/src/infra/channel-runtime-context.ts @@ -83,10 +83,10 @@ export function watchChannelRuntimeContexts( }); } -export function createTaskScopedChannelRuntime(params: { - channelRuntime?: ChannelRuntimeSurface; +export function createTaskScopedChannelRuntime(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,