Plugins: split message discovery and dispatch

This commit is contained in:
Gustavo Madeira Santana 2026-03-18 00:15:58 +00:00
parent da948a8073
commit 951f3f992b
No known key found for this signature in database
10 changed files with 374 additions and 383 deletions

View File

@ -1,12 +1,10 @@
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
import {
createMessageActionDiscoveryContext,
resolveMessageActionDiscoveryChannelId,
} from "../channels/plugins/message-action-discovery.js";
import {
__testing as messageActionTesting,
resolveMessageActionDiscoveryForPlugin,
} from "../channels/plugins/message-actions.js";
resolveMessageActionDiscoveryChannelId,
__testing as messageActionTesting,
} from "../channels/plugins/message-action-discovery.js";
import type { ChannelAgentTool, ChannelMessageActionName } from "../channels/plugins/types.js";
import { normalizeAnyChannelId } from "../channels/registry.js";
import type { OpenClawConfig } from "../config/config.js";
@ -32,7 +30,7 @@ export function listChannelSupportedActions(params: {
return [];
}
const plugin = getChannelPlugin(channelId as Parameters<typeof getChannelPlugin>[0]);
if (!plugin?.actions?.listActions) {
if (!plugin?.actions) {
return [];
}
return resolveMessageActionDiscoveryForPlugin({

View File

@ -5,7 +5,7 @@ import {
channelSupportsMessageCapabilityForChannel,
listChannelMessageActions,
resolveChannelMessageToolSchemaProperties,
} from "../../channels/plugins/message-actions.js";
} from "../../channels/plugins/message-action-discovery.js";
import type { ChannelMessageCapability } from "../../channels/plugins/message-capabilities.js";
import {
CHANNEL_MESSAGE_ACTION_NAMES,

View File

@ -1,6 +1,15 @@
import type { TSchema } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/config.js";
import { defaultRuntime } from "../../runtime.js";
import { normalizeAnyChannelId } from "../registry.js";
import type { ChannelMessageActionDiscoveryContext } from "./types.js";
import { getChannelPlugin, listChannelPlugins } from "./index.js";
import type { ChannelMessageCapability } from "./message-capabilities.js";
import type {
ChannelMessageActionDiscoveryContext,
ChannelMessageActionName,
ChannelMessageToolDiscovery,
ChannelMessageToolSchemaContribution,
} from "./types.js";
export type ChannelMessageActionDiscoveryInput = {
cfg?: OpenClawConfig;
@ -16,6 +25,10 @@ export type ChannelMessageActionDiscoveryInput = {
requesterSenderId?: string | null;
};
type ChannelActions = NonNullable<NonNullable<ReturnType<typeof getChannelPlugin>>["actions"]>;
const loggedMessageActionErrors = new Set<string>();
export function resolveMessageActionDiscoveryChannelId(raw?: string | null): string | undefined {
const normalized = normalizeAnyChannelId(raw);
if (normalized) {
@ -44,3 +57,322 @@ export function createMessageActionDiscoveryContext(
requesterSenderId: params.requesterSenderId,
};
}
function logMessageActionError(params: {
pluginId: string;
operation: "describeMessageTool" | "getCapabilities" | "getToolSchema" | "listActions";
error: unknown;
}) {
const message = params.error instanceof Error ? params.error.message : String(params.error);
const key = `${params.pluginId}:${params.operation}:${message}`;
if (loggedMessageActionErrors.has(key)) {
return;
}
loggedMessageActionErrors.add(key);
const stack = params.error instanceof Error && params.error.stack ? params.error.stack : null;
defaultRuntime.error?.(
`[message-action-discovery] ${params.pluginId}.actions.${params.operation} failed: ${stack ?? message}`,
);
}
function runListActionsSafely(params: {
pluginId: string;
context: ChannelMessageActionDiscoveryContext;
listActions: NonNullable<ChannelActions["listActions"]>;
}): ChannelMessageActionName[] {
try {
const listed = params.listActions(params.context);
return Array.isArray(listed) ? listed : [];
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "listActions",
error,
});
return [];
}
}
function describeMessageToolSafely(params: {
pluginId: string;
context: ChannelMessageActionDiscoveryContext;
describeMessageTool: NonNullable<ChannelActions["describeMessageTool"]>;
}): ChannelMessageToolDiscovery | null {
try {
return params.describeMessageTool(params.context) ?? null;
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "describeMessageTool",
error,
});
return null;
}
}
function listCapabilitiesSafely(params: {
pluginId: string;
actions: ChannelActions;
context: ChannelMessageActionDiscoveryContext;
}): readonly ChannelMessageCapability[] {
try {
return params.actions.getCapabilities?.(params.context) ?? [];
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "getCapabilities",
error,
});
return [];
}
}
function runGetToolSchemaSafely(params: {
pluginId: string;
context: ChannelMessageActionDiscoveryContext;
getToolSchema: NonNullable<ChannelActions["getToolSchema"]>;
}):
| ChannelMessageToolSchemaContribution
| ChannelMessageToolSchemaContribution[]
| null
| undefined {
try {
return params.getToolSchema(params.context);
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "getToolSchema",
error,
});
return null;
}
}
function normalizeToolSchemaContributions(
value:
| ChannelMessageToolSchemaContribution
| ChannelMessageToolSchemaContribution[]
| null
| undefined,
): ChannelMessageToolSchemaContribution[] {
if (!value) {
return [];
}
return Array.isArray(value) ? value : [value];
}
type ResolvedChannelMessageActionDiscovery = {
actions: ChannelMessageActionName[];
capabilities: readonly ChannelMessageCapability[];
schemaContributions: ChannelMessageToolSchemaContribution[];
};
export function resolveMessageActionDiscoveryForPlugin(params: {
pluginId: string;
actions?: ChannelActions;
context: ChannelMessageActionDiscoveryContext;
includeActions?: boolean;
includeCapabilities?: boolean;
includeSchema?: boolean;
}): ResolvedChannelMessageActionDiscovery {
const adapter = params.actions;
if (!adapter) {
return {
actions: [],
capabilities: [],
schemaContributions: [],
};
}
if (adapter.describeMessageTool) {
const described = describeMessageToolSafely({
pluginId: params.pluginId,
context: params.context,
describeMessageTool: adapter.describeMessageTool,
});
return {
actions:
params.includeActions && Array.isArray(described?.actions) ? [...described.actions] : [],
capabilities:
params.includeCapabilities && Array.isArray(described?.capabilities)
? described.capabilities
: [],
schemaContributions: params.includeSchema
? normalizeToolSchemaContributions(described?.schema)
: [],
};
}
return {
actions:
params.includeActions && adapter.listActions
? runListActionsSafely({
pluginId: params.pluginId,
context: params.context,
listActions: adapter.listActions,
})
: [],
capabilities:
params.includeCapabilities && adapter.getCapabilities
? listCapabilitiesSafely({
pluginId: params.pluginId,
actions: adapter,
context: params.context,
})
: [],
schemaContributions:
params.includeSchema && adapter.getToolSchema
? normalizeToolSchemaContributions(
runGetToolSchemaSafely({
pluginId: params.pluginId,
context: params.context,
getToolSchema: adapter.getToolSchema,
}),
)
: [],
};
}
export function listChannelMessageActions(cfg: OpenClawConfig): ChannelMessageActionName[] {
const actions = new Set<ChannelMessageActionName>(["send", "broadcast"]);
for (const plugin of listChannelPlugins()) {
for (const action of resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: { cfg },
includeActions: true,
}).actions) {
actions.add(action);
}
}
return Array.from(actions);
}
export function listChannelMessageCapabilities(cfg: OpenClawConfig): ChannelMessageCapability[] {
const capabilities = new Set<ChannelMessageCapability>();
for (const plugin of listChannelPlugins()) {
for (const capability of resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: { cfg },
includeCapabilities: true,
}).capabilities) {
capabilities.add(capability);
}
}
return Array.from(capabilities);
}
export function listChannelMessageCapabilitiesForChannel(params: {
cfg: OpenClawConfig;
channel?: string;
currentChannelId?: string | null;
currentThreadTs?: string | null;
currentMessageId?: string | number | null;
accountId?: string | null;
sessionKey?: string | null;
sessionId?: string | null;
agentId?: string | null;
requesterSenderId?: string | null;
}): ChannelMessageCapability[] {
const channelId = resolveMessageActionDiscoveryChannelId(params.channel);
if (!channelId) {
return [];
}
const plugin = getChannelPlugin(channelId as Parameters<typeof getChannelPlugin>[0]);
return plugin?.actions
? Array.from(
resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: createMessageActionDiscoveryContext(params),
includeCapabilities: true,
}).capabilities,
)
: [];
}
function mergeToolSchemaProperties(
target: Record<string, TSchema>,
source: Record<string, TSchema> | undefined,
) {
if (!source) {
return;
}
for (const [name, schema] of Object.entries(source)) {
if (!(name in target)) {
target[name] = schema;
}
}
}
export function resolveChannelMessageToolSchemaProperties(params: {
cfg: OpenClawConfig;
channel?: string;
currentChannelId?: string | null;
currentThreadTs?: string | null;
currentMessageId?: string | number | null;
accountId?: string | null;
sessionKey?: string | null;
sessionId?: string | null;
agentId?: string | null;
requesterSenderId?: string | null;
}): Record<string, TSchema> {
const properties: Record<string, TSchema> = {};
const currentChannel = resolveMessageActionDiscoveryChannelId(params.channel);
const discoveryBase = createMessageActionDiscoveryContext(params);
for (const plugin of listChannelPlugins()) {
if (!plugin.actions) {
continue;
}
for (const contribution of resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: discoveryBase,
includeSchema: true,
}).schemaContributions) {
const visibility = contribution.visibility ?? "current-channel";
if (currentChannel) {
if (visibility === "all-configured" || plugin.id === currentChannel) {
mergeToolSchemaProperties(properties, contribution.properties);
}
continue;
}
mergeToolSchemaProperties(properties, contribution.properties);
}
}
return properties;
}
export function channelSupportsMessageCapability(
cfg: OpenClawConfig,
capability: ChannelMessageCapability,
): boolean {
return listChannelMessageCapabilities(cfg).includes(capability);
}
export function channelSupportsMessageCapabilityForChannel(
params: {
cfg: OpenClawConfig;
channel?: string;
currentChannelId?: string | null;
currentThreadTs?: string | null;
currentMessageId?: string | number | null;
accountId?: string | null;
sessionKey?: string | null;
sessionId?: string | null;
agentId?: string | null;
requesterSenderId?: string | null;
},
capability: ChannelMessageCapability,
): boolean {
return listChannelMessageCapabilitiesForChannel(params).includes(capability);
}
export const __testing = {
resetLoggedMessageActionErrors() {
loggedMessageActionErrors.clear();
},
};

View File

@ -0,0 +1,31 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { getChannelPlugin } from "./index.js";
import type { ChannelMessageActionContext } from "./types.js";
function requiresTrustedRequesterSender(ctx: ChannelMessageActionContext): boolean {
const plugin = getChannelPlugin(ctx.channel);
return Boolean(
plugin?.actions?.requiresTrustedRequesterSender?.({
action: ctx.action,
toolContext: ctx.toolContext,
}),
);
}
export async function dispatchChannelMessageAction(
ctx: ChannelMessageActionContext,
): Promise<AgentToolResult<unknown> | null> {
if (requiresTrustedRequesterSender(ctx) && !ctx.requesterSenderId?.trim()) {
throw new Error(
`Trusted sender identity is required for ${ctx.channel}:${ctx.action} in tool-driven contexts.`,
);
}
const plugin = getChannelPlugin(ctx.channel);
if (!plugin?.actions?.handleAction) {
return null;
}
if (plugin.actions.supportsAction && !plugin.actions.supportsAction({ action: ctx.action })) {
return null;
}
return await plugin.actions.handleAction(ctx);
}

View File

@ -6,7 +6,7 @@ import {
createChannelTestPluginBase,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { dispatchChannelMessageAction } from "./message-actions.js";
import { dispatchChannelMessageAction } from "./message-action-dispatch.js";
import type { ChannelPlugin } from "./types.js";
const handleAction = vi.fn(async () => jsonResult({ ok: true }));

View File

@ -15,7 +15,7 @@ import {
listChannelMessageCapabilities,
listChannelMessageCapabilitiesForChannel,
resolveChannelMessageToolSchemaProperties,
} from "./message-actions.js";
} from "./message-action-discovery.js";
import type { ChannelMessageCapability } from "./message-capabilities.js";
import type { ChannelPlugin } from "./types.js";

View File

@ -1,370 +0,0 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { TSchema } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/config.js";
import { defaultRuntime } from "../../runtime.js";
import { getChannelPlugin, listChannelPlugins } from "./index.js";
import {
createMessageActionDiscoveryContext,
resolveMessageActionDiscoveryChannelId,
} from "./message-action-discovery.js";
import type { ChannelMessageCapability } from "./message-capabilities.js";
import type {
ChannelMessageActionContext,
ChannelMessageActionDiscoveryContext,
ChannelMessageActionName,
ChannelMessageToolDiscovery,
ChannelMessageToolSchemaContribution,
} from "./types.js";
type ChannelActions = NonNullable<NonNullable<ReturnType<typeof getChannelPlugin>>["actions"]>;
function requiresTrustedRequesterSender(ctx: ChannelMessageActionContext): boolean {
const plugin = getChannelPlugin(ctx.channel);
return Boolean(
plugin?.actions?.requiresTrustedRequesterSender?.({
action: ctx.action,
toolContext: ctx.toolContext,
}),
);
}
const loggedMessageActionErrors = new Set<string>();
function logMessageActionError(params: {
pluginId: string;
operation: "describeMessageTool" | "getCapabilities" | "getToolSchema" | "listActions";
error: unknown;
}) {
const message = params.error instanceof Error ? params.error.message : String(params.error);
const key = `${params.pluginId}:${params.operation}:${message}`;
if (loggedMessageActionErrors.has(key)) {
return;
}
loggedMessageActionErrors.add(key);
const stack = params.error instanceof Error && params.error.stack ? params.error.stack : null;
defaultRuntime.error?.(
`[message-actions] ${params.pluginId}.actions.${params.operation} failed: ${stack ?? message}`,
);
}
function runListActionsSafely(params: {
pluginId: string;
context: ChannelMessageActionDiscoveryContext;
listActions: NonNullable<ChannelActions["listActions"]>;
}): ChannelMessageActionName[] {
try {
const listed = params.listActions(params.context);
return Array.isArray(listed) ? listed : [];
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "listActions",
error,
});
return [];
}
}
function describeMessageToolSafely(params: {
pluginId: string;
context: ChannelMessageActionDiscoveryContext;
describeMessageTool: NonNullable<ChannelActions["describeMessageTool"]>;
}): ChannelMessageToolDiscovery | null {
try {
return params.describeMessageTool(params.context) ?? null;
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "describeMessageTool",
error,
});
return null;
}
}
function listCapabilities(params: {
pluginId: string;
actions: ChannelActions;
context: ChannelMessageActionDiscoveryContext;
}): readonly ChannelMessageCapability[] {
try {
return params.actions.getCapabilities?.(params.context) ?? [];
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "getCapabilities",
error,
});
return [];
}
}
function normalizeToolSchemaContributions(
value:
| ChannelMessageToolSchemaContribution
| ChannelMessageToolSchemaContribution[]
| null
| undefined,
): ChannelMessageToolSchemaContribution[] {
if (!value) {
return [];
}
return Array.isArray(value) ? value : [value];
}
type ResolvedChannelMessageActionDiscovery = {
actions: ChannelMessageActionName[];
capabilities: readonly ChannelMessageCapability[];
schemaContributions: ChannelMessageToolSchemaContribution[];
};
export function resolveMessageActionDiscoveryForPlugin(params: {
pluginId: string;
actions?: ChannelActions;
context: ChannelMessageActionDiscoveryContext;
includeActions?: boolean;
includeCapabilities?: boolean;
includeSchema?: boolean;
}): ResolvedChannelMessageActionDiscovery {
const adapter = params.actions;
if (!adapter) {
return {
actions: [],
capabilities: [],
schemaContributions: [],
};
}
if (adapter.describeMessageTool) {
const described = describeMessageToolSafely({
pluginId: params.pluginId,
context: params.context,
describeMessageTool: adapter.describeMessageTool,
});
return {
actions:
params.includeActions && Array.isArray(described?.actions) ? [...described.actions] : [],
capabilities:
params.includeCapabilities && Array.isArray(described?.capabilities)
? described.capabilities
: [],
schemaContributions: params.includeSchema
? normalizeToolSchemaContributions(described?.schema)
: [],
};
}
return {
actions:
params.includeActions && adapter.listActions
? runListActionsSafely({
pluginId: params.pluginId,
context: params.context,
listActions: adapter.listActions,
})
: [],
capabilities:
params.includeCapabilities && adapter.getCapabilities
? listCapabilities({
pluginId: params.pluginId,
actions: adapter,
context: params.context,
})
: [],
schemaContributions:
params.includeSchema && adapter.getToolSchema
? normalizeToolSchemaContributions(
runGetToolSchemaSafely({
pluginId: params.pluginId,
context: params.context,
getToolSchema: adapter.getToolSchema,
}),
)
: [],
};
}
export function listChannelMessageActions(cfg: OpenClawConfig): ChannelMessageActionName[] {
const actions = new Set<ChannelMessageActionName>(["send", "broadcast"]);
for (const plugin of listChannelPlugins()) {
for (const action of resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: { cfg },
includeActions: true,
}).actions) {
actions.add(action);
}
}
return Array.from(actions);
}
function runGetToolSchemaSafely(params: {
pluginId: string;
context: ChannelMessageActionDiscoveryContext;
getToolSchema: NonNullable<ChannelActions["getToolSchema"]>;
}):
| ChannelMessageToolSchemaContribution
| ChannelMessageToolSchemaContribution[]
| null
| undefined {
try {
return params.getToolSchema(params.context);
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "getToolSchema",
error,
});
return null;
}
}
export function listChannelMessageCapabilities(cfg: OpenClawConfig): ChannelMessageCapability[] {
const capabilities = new Set<ChannelMessageCapability>();
for (const plugin of listChannelPlugins()) {
for (const capability of resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: { cfg },
includeCapabilities: true,
}).capabilities) {
capabilities.add(capability);
}
}
return Array.from(capabilities);
}
export function listChannelMessageCapabilitiesForChannel(params: {
cfg: OpenClawConfig;
channel?: string;
currentChannelId?: string | null;
currentThreadTs?: string | null;
currentMessageId?: string | number | null;
accountId?: string | null;
sessionKey?: string | null;
sessionId?: string | null;
agentId?: string | null;
requesterSenderId?: string | null;
}): ChannelMessageCapability[] {
const channelId = resolveMessageActionDiscoveryChannelId(params.channel);
if (!channelId) {
return [];
}
const plugin = getChannelPlugin(channelId as Parameters<typeof getChannelPlugin>[0]);
return plugin?.actions
? Array.from(
resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: createMessageActionDiscoveryContext(params),
includeCapabilities: true,
}).capabilities,
)
: [];
}
function mergeToolSchemaProperties(
target: Record<string, TSchema>,
source: Record<string, TSchema> | undefined,
) {
if (!source) {
return;
}
for (const [name, schema] of Object.entries(source)) {
if (!(name in target)) {
target[name] = schema;
}
}
}
export function resolveChannelMessageToolSchemaProperties(params: {
cfg: OpenClawConfig;
channel?: string;
currentChannelId?: string | null;
currentThreadTs?: string | null;
currentMessageId?: string | number | null;
accountId?: string | null;
sessionKey?: string | null;
sessionId?: string | null;
agentId?: string | null;
requesterSenderId?: string | null;
}): Record<string, TSchema> {
const properties: Record<string, TSchema> = {};
const plugins = listChannelPlugins();
const currentChannel = resolveMessageActionDiscoveryChannelId(params.channel);
const discoveryBase: ChannelMessageActionDiscoveryContext =
createMessageActionDiscoveryContext(params);
for (const plugin of plugins) {
if (!plugin?.actions) {
continue;
}
for (const contribution of resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: discoveryBase,
includeSchema: true,
}).schemaContributions) {
const visibility = contribution.visibility ?? "current-channel";
if (currentChannel) {
if (visibility === "all-configured" || plugin.id === currentChannel) {
mergeToolSchemaProperties(properties, contribution.properties);
}
continue;
}
mergeToolSchemaProperties(properties, contribution.properties);
}
}
return properties;
}
export function channelSupportsMessageCapability(
cfg: OpenClawConfig,
capability: ChannelMessageCapability,
): boolean {
return listChannelMessageCapabilities(cfg).includes(capability);
}
export function channelSupportsMessageCapabilityForChannel(
params: {
cfg: OpenClawConfig;
channel?: string;
currentChannelId?: string | null;
currentThreadTs?: string | null;
currentMessageId?: string | number | null;
accountId?: string | null;
sessionKey?: string | null;
sessionId?: string | null;
agentId?: string | null;
requesterSenderId?: string | null;
},
capability: ChannelMessageCapability,
): boolean {
return listChannelMessageCapabilitiesForChannel(params).includes(capability);
}
export async function dispatchChannelMessageAction(
ctx: ChannelMessageActionContext,
): Promise<AgentToolResult<unknown> | null> {
if (requiresTrustedRequesterSender(ctx) && !ctx.requesterSenderId?.trim()) {
throw new Error(
`Trusted sender identity is required for ${ctx.channel}:${ctx.action} in tool-driven contexts.`,
);
}
const plugin = getChannelPlugin(ctx.channel);
if (!plugin?.actions?.handleAction) {
return null;
}
if (plugin.actions.supportsAction && !plugin.actions.supportsAction({ action: ctx.action })) {
return null;
}
return await plugin.actions.handleAction(ctx);
}
export const __testing = {
resetLoggedMessageActionErrors() {
loggedMessageActionErrors.clear();
},
};

View File

@ -7,7 +7,7 @@ import {
} from "../../agents/tools/common.js";
import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js";
import { getChannelPlugin } from "../../channels/plugins/index.js";
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
import { dispatchChannelMessageAction } from "../../channels/plugins/message-action-dispatch.js";
import type {
ChannelId,
ChannelMessageActionName,

View File

@ -10,7 +10,7 @@ const mocks = vi.hoisted(() => ({
appendAssistantMessageToSessionTranscript: vi.fn(async () => ({ ok: true, sessionFile: "x" })),
}));
vi.mock("../../channels/plugins/message-actions.js", () => ({
vi.mock("../../channels/plugins/message-action-dispatch.js", () => ({
dispatchChannelMessageAction: mocks.dispatchChannelMessageAction,
}));

View File

@ -1,5 +1,5 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
import { dispatchChannelMessageAction } from "../../channels/plugins/message-action-dispatch.js";
import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { appendAssistantMessageToSessionTranscript } from "../../config/sessions.js";