fix(cycles): narrow channel runtime surface

This commit is contained in:
Vincent Koc
2026-04-11 19:28:05 +01:00
parent 26f633b604
commit 462d8e3bc0
11 changed files with 77 additions and 33 deletions

View File

@@ -9,6 +9,7 @@ import {
import { GatewayCloseCodes, type GatewayPlugin } from "@buape/carbon/gateway";
import { Routes } from "discord-api-types/v10";
import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
import {
listNativeCommandSpecsForConfig,
@@ -95,7 +96,7 @@ export type MonitorDiscordOpts = {
accountId?: string;
config?: OpenClawConfig;
runtime?: RuntimeEnv;
channelRuntime?: import("openclaw/plugin-sdk/channel-core").PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
abortSignal?: AbortSignal;
mediaMaxMb?: number;
historyLimit?: number;

View File

@@ -1,5 +1,6 @@
import { format } from "node:util";
import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
import {
@@ -45,7 +46,7 @@ import { createMatrixMonitorTaskRunner } from "./task-runner.js";
export type MonitorMatrixOpts = {
runtime?: RuntimeEnv;
channelRuntime?: import("openclaw/plugin-sdk/channel-core").PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
abortSignal?: AbortSignal;
mediaMaxMb?: number;
initialSyncLimit?: number;

View File

@@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig, SlackSlashCommandConfig } from "openclaw/plugin-sdk/config-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import type { SlackFile, SlackMessageEvent } from "../types.js";
@@ -10,7 +10,7 @@ export type MonitorSlackOpts = {
mode?: "socket" | "http";
config?: OpenClawConfig;
runtime?: RuntimeEnv;
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
abortSignal?: AbortSignal;
mediaMaxMb?: number;
slashCommand?: SlackSlashCommandConfig;

View File

@@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
@@ -7,7 +7,7 @@ export type MonitorTelegramOpts = {
accountId?: string;
config?: OpenClawConfig;
runtime?: RuntimeEnv;
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
abortSignal?: AbortSignal;
useWebhook?: boolean;
webhookPath?: string;

View File

@@ -0,0 +1,43 @@
export type ChannelRuntimeContextKey = {
channelId: string;
accountId?: string | null;
capability: string;
};
export type ChannelRuntimeContextEvent = {
type: "registered" | "unregistered";
key: {
channelId: string;
accountId?: string;
capability: string;
};
context?: unknown;
};
export type ChannelRuntimeContextRegistry = {
register: (
params: ChannelRuntimeContextKey & {
context: unknown;
abortSignal?: AbortSignal;
},
) => { dispose: () => void };
get: <T = unknown>(params: ChannelRuntimeContextKey) => T | undefined;
watch: (params: {
channelId?: string;
accountId?: string | null;
capability?: string;
onEvent: (event: ChannelRuntimeContextEvent) => void;
}) => () => void;
};
/**
* Minimal channel-runtime surface threaded through gateway/setup flows.
*
* 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.
*/
export type ChannelRuntimeSurface = {
runtimeContexts: ChannelRuntimeContextRegistry;
[key: string]: unknown;
};

View File

@@ -10,11 +10,11 @@ import type {
PluginApprovalRequest,
PluginApprovalResolved,
} from "../../infra/plugin-approvals.js";
import type { PluginRuntime } from "../../plugins/runtime/types.js";
import type { RuntimeEnv } from "../../runtime.js";
import type { ResolverContext, SecretDefaults } from "../../secrets/runtime-shared.js";
import type { SecretTargetRegistryEntry } from "../../secrets/target-registry-types.js";
import type { ChannelApprovalNativeAdapter } from "./approval-native.types.js";
import type { ChannelRuntimeSurface } from "./channel-runtime-surface.types.js";
import type { ConfigWriteTarget } from "./config-writes.js";
export type {
ChannelOutboundAdapter,
@@ -305,7 +305,7 @@ export type ChannelGatewayContext<ResolvedAccount = unknown> = {
* @since Plugin SDK 2026.2.19
* @see {@link https://docs.openclaw.ai/plugins/developing-plugins | Plugin SDK documentation}
*/
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
};
export type ChannelLogoutResult = {

View File

@@ -48,6 +48,7 @@ export type {
ChannelSetupAdapter,
ChannelStatusAdapter,
} from "./types.adapters.js";
export type { ChannelRuntimeSurface } from "./channel-runtime-surface.types.js";
export type {
ChannelAccountSnapshot,
ChannelAccountState,

View File

@@ -1,3 +1,4 @@
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
import { type ChannelId, getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
import type { ChannelAccountSnapshot } from "../channels/plugins/types.public.js";
@@ -8,7 +9,6 @@ import { createTaskScopedChannelRuntime } from "../infra/channel-runtime-context
import { formatErrorMessage } from "../infra/errors.js";
import { resetDirectoryCache } from "../infra/outbound/target-resolver.js";
import type { createSubsystemLogger } from "../logging/subsystem.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import { resolveAccountEntry, resolveNormalizedAccountEntry } from "../routing/account-lookup.js";
import {
DEFAULT_ACCOUNT_ID,
@@ -125,7 +125,7 @@ type ChannelManagerOptions = {
* @since Plugin SDK 2026.2.19
* @see {@link ChannelGatewayContext.channelRuntime}
*/
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
/**
* Lazily resolves optional channel runtime helpers for external channel plugins.
*
@@ -134,7 +134,7 @@ type ChannelManagerOptions = {
* a channel account actually starts. The resolved value must be a real
* `createPluginRuntime().channel` surface.
*/
resolveChannelRuntime?: () => PluginRuntime["channel"];
resolveChannelRuntime?: () => ChannelRuntimeSurface;
};
type StartChannelOptions = {
@@ -252,7 +252,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
return next;
};
const getChannelRuntime = (): PluginRuntime["channel"] | undefined => {
const getChannelRuntime = (): ChannelRuntimeSurface | undefined => {
return channelRuntime ?? resolveChannelRuntime?.();
};
@@ -299,7 +299,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
let handedOffTask = false;
const log = channelLogs[channelId];
let scopedChannelRuntime: ReturnType<typeof createTaskScopedChannelRuntime> | null = null;
let channelRuntimeForTask: PluginRuntime["channel"] | undefined;
let channelRuntimeForTask: ChannelRuntimeSurface | undefined;
let stopApprovalBootstrap: () => Promise<void> = async () => {};
const stopTaskScopedApprovalRuntime = async () => {
const scopedRuntime = scopedChannelRuntime;

View File

@@ -1,8 +1,8 @@
import { resolveChannelApprovalCapability } from "../channels/plugins/approvals.js";
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import {
CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
createChannelApprovalHandlerFromCapability,
@@ -20,7 +20,7 @@ export async function startChannelApprovalHandlerBootstrap(params: {
plugin: Pick<ChannelPlugin, "id" | "meta" | "approvalCapability">;
cfg: OpenClawConfig;
accountId: string;
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
logger?: ReturnType<typeof createSubsystemLogger>;
}): Promise<() => Promise<void>> {
const capability = resolveChannelApprovalCapability(params.plugin);

View File

@@ -1,16 +1,13 @@
import type { PluginRuntime } from "../plugins/runtime/types.js";
export type ChannelRuntimeContextKey = {
channelId: string;
accountId?: string | null;
capability: string;
};
import type {
ChannelRuntimeContextKey,
ChannelRuntimeSurface,
} from "../channels/plugins/channel-runtime-surface.types.js";
const NOOP_DISPOSE = () => {};
function resolveScopedRuntimeContextRegistry(params: {
channelRuntime: PluginRuntime["channel"];
}): PluginRuntime["channel"]["runtimeContexts"] {
channelRuntime: ChannelRuntimeSurface;
}): ChannelRuntimeSurface["runtimeContexts"] {
const runtimeContexts = resolveRuntimeContextRegistry(params);
if (
runtimeContexts &&
@@ -26,14 +23,14 @@ function resolveScopedRuntimeContextRegistry(params: {
}
function resolveRuntimeContextRegistry(params: {
channelRuntime?: PluginRuntime["channel"];
}): PluginRuntime["channel"]["runtimeContexts"] | null {
channelRuntime?: ChannelRuntimeSurface;
}): ChannelRuntimeSurface["runtimeContexts"] | null {
return params.channelRuntime?.runtimeContexts ?? null;
}
export function registerChannelRuntimeContext(
params: ChannelRuntimeContextKey & {
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
context: unknown;
abortSignal?: AbortSignal;
},
@@ -53,7 +50,7 @@ export function registerChannelRuntimeContext(
export function getChannelRuntimeContext<T = unknown>(
params: ChannelRuntimeContextKey & {
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
},
): T | undefined {
const runtimeContexts = resolveRuntimeContextRegistry(params);
@@ -69,8 +66,8 @@ export function getChannelRuntimeContext<T = unknown>(
export function watchChannelRuntimeContexts(
params: ChannelRuntimeContextKey & {
channelRuntime?: PluginRuntime["channel"];
onEvent: Parameters<PluginRuntime["channel"]["runtimeContexts"]["watch"]>[0]["onEvent"];
channelRuntime?: ChannelRuntimeSurface;
onEvent: Parameters<ChannelRuntimeSurface["runtimeContexts"]["watch"]>[0]["onEvent"];
},
): (() => void) | null {
const runtimeContexts = resolveRuntimeContextRegistry(params);
@@ -86,9 +83,9 @@ export function watchChannelRuntimeContexts(
}
export function createTaskScopedChannelRuntime(params: {
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
}): {
channelRuntime?: PluginRuntime["channel"];
channelRuntime?: ChannelRuntimeSurface;
dispose: () => void;
} {
const baseRuntime = params.channelRuntime;
@@ -116,7 +113,7 @@ export function createTaskScopedChannelRuntime(params: {
};
};
const scopedRuntime: PluginRuntime["channel"] = {
const scopedRuntime: ChannelRuntimeSurface = {
...baseRuntime,
runtimeContexts: {
...runtimeContexts,

View File

@@ -35,3 +35,4 @@ export type {
ChannelGatewayContext,
ChannelOutboundAdapter,
} from "../channels/plugins/types.adapters.js";
export type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";