From 690c58baa29aa2eba79f459df2221a94d949b3da Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 3 Apr 2026 23:22:26 +0900 Subject: [PATCH] refactor(discord): lazy-load subagent hooks --- extensions/discord/index.ts | 25 +- extensions/discord/src/subagent-hooks.ts | 288 +++++++++++++---------- 2 files changed, 185 insertions(+), 128 deletions(-) diff --git a/extensions/discord/index.ts b/extensions/discord/index.ts index 6d3c754edb4..7328b96015d 100644 --- a/extensions/discord/index.ts +++ b/extensions/discord/index.ts @@ -1,16 +1,37 @@ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { discordPlugin } from "./src/channel.js"; import { setDiscordRuntime } from "./src/runtime.js"; -import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js"; export { discordPlugin } from "./src/channel.js"; export { setDiscordRuntime } from "./src/runtime.js"; +type DiscordSubagentHooksModule = typeof import("./src/subagent-hooks.js"); + +let discordSubagentHooksPromise: Promise | null = null; + +function loadDiscordSubagentHooksModule() { + discordSubagentHooksPromise ??= import("./src/subagent-hooks.js"); + return discordSubagentHooksPromise; +} + export default defineChannelPluginEntry({ id: "discord", name: "Discord", description: "Discord channel plugin", plugin: discordPlugin, setRuntime: setDiscordRuntime, - registerFull: registerDiscordSubagentHooks, + registerFull(api) { + api.on("subagent_spawning", async (event) => { + const { handleDiscordSubagentSpawning } = await loadDiscordSubagentHooksModule(); + return await handleDiscordSubagentSpawning(api, event); + }); + api.on("subagent_ended", async (event) => { + const { handleDiscordSubagentEnded } = await loadDiscordSubagentHooksModule(); + handleDiscordSubagentEnded(event); + }); + api.on("subagent_delivery_target", async (event) => { + const { handleDiscordSubagentDeliveryTarget } = await loadDiscordSubagentHooksModule(); + return handleDiscordSubagentDeliveryTarget(event); + }); + }, }); diff --git a/extensions/discord/src/subagent-hooks.ts b/extensions/discord/src/subagent-hooks.ts index c9ba7b97984..fd9e7ee2f1f 100644 --- a/extensions/discord/src/subagent-hooks.ts +++ b/extensions/discord/src/subagent-hooks.ts @@ -16,137 +16,173 @@ function summarizeError(err: unknown): string { return "error"; } -export function registerDiscordSubagentHooks(api: OpenClawPluginApi) { - const resolveThreadBindingFlags = (accountId?: string) => { - const account = resolveDiscordAccount({ - cfg: api.config, - accountId, - }); - const baseThreadBindings = api.config.channels?.discord?.threadBindings; - const accountThreadBindings = - api.config.channels?.discord?.accounts?.[account.accountId]?.threadBindings; - return { - enabled: - accountThreadBindings?.enabled ?? - baseThreadBindings?.enabled ?? - api.config.session?.threadBindings?.enabled ?? - true, - spawnSubagentSessions: - accountThreadBindings?.spawnSubagentSessions ?? - baseThreadBindings?.spawnSubagentSessions ?? - false, - }; +type DiscordSubagentSpawningEvent = { + threadRequested?: boolean; + requester?: { + channel?: string; + accountId?: string; + to?: string; + threadId?: string | number; }; + childSessionKey: string; + agentId?: string; + label?: string; +}; - api.on("subagent_spawning", async (event) => { - if (!event.threadRequested) { - return; - } - const channel = event.requester?.channel?.trim().toLowerCase(); - if (channel !== "discord") { - // Ignore non-Discord channels so channel-specific plugins can handle - // their own thread/session provisioning without Discord blocking them. - return; - } - const threadBindingFlags = resolveThreadBindingFlags(event.requester?.accountId); - if (!threadBindingFlags.enabled) { - return { - status: "error" as const, - error: - "Discord thread bindings are disabled (set channels.discord.threadBindings.enabled=true to override for this account, or session.threadBindings.enabled=true globally).", - }; - } - if (!threadBindingFlags.spawnSubagentSessions) { - return { - status: "error" as const, - error: - "Discord thread-bound subagent spawns are disabled for this account (set channels.discord.threadBindings.spawnSubagentSessions=true to enable).", - }; - } - try { - const binding = await autoBindSpawnedDiscordSubagent({ - accountId: event.requester?.accountId, - channel: event.requester?.channel, - to: event.requester?.to, - threadId: event.requester?.threadId, - childSessionKey: event.childSessionKey, - agentId: event.agentId, - label: event.label, - boundBy: "system", - }); - if (!binding) { - return { - status: "error" as const, - error: - "Unable to create or bind a Discord thread for this subagent session. Session mode is unavailable for this target.", - }; - } - return { status: "ok" as const, threadBindingReady: true }; - } catch (err) { - return { - status: "error" as const, - error: `Discord thread bind failed: ${summarizeError(err)}`, - }; - } +type DiscordSubagentEndedEvent = { + targetSessionKey: string; + accountId?: string; + targetKind?: string; + reason?: string; + sendFarewell?: boolean; +}; + +type DiscordSubagentDeliveryTargetEvent = { + expectsCompletionMessage?: boolean; + childSessionKey: string; + requesterOrigin?: { + channel?: string; + accountId?: string; + threadId?: string | number; + }; +}; + +function resolveThreadBindingFlags(api: OpenClawPluginApi, accountId?: string) { + const account = resolveDiscordAccount({ + cfg: api.config, + accountId, }); + const baseThreadBindings = api.config.channels?.discord?.threadBindings; + const accountThreadBindings = + api.config.channels?.discord?.accounts?.[account.accountId]?.threadBindings; + return { + enabled: + accountThreadBindings?.enabled ?? + baseThreadBindings?.enabled ?? + api.config.session?.threadBindings?.enabled ?? + true, + spawnSubagentSessions: + accountThreadBindings?.spawnSubagentSessions ?? + baseThreadBindings?.spawnSubagentSessions ?? + false, + }; +} - api.on("subagent_ended", (event) => { - unbindThreadBindingsBySessionKey({ - targetSessionKey: event.targetSessionKey, - accountId: event.accountId, - targetKind: event.targetKind, - reason: event.reason, - sendFarewell: event.sendFarewell, - }); - }); - - api.on("subagent_delivery_target", (event) => { - if (!event.expectsCompletionMessage) { - return; - } - const requesterChannel = event.requesterOrigin?.channel?.trim().toLowerCase(); - if (requesterChannel !== "discord") { - return; - } - const requesterAccountId = event.requesterOrigin?.accountId?.trim(); - const requesterThreadId = - event.requesterOrigin?.threadId != null && event.requesterOrigin.threadId !== "" - ? String(event.requesterOrigin.threadId).trim() - : ""; - const bindings = listThreadBindingsBySessionKey({ - targetSessionKey: event.childSessionKey, - ...(requesterAccountId ? { accountId: requesterAccountId } : {}), - targetKind: "subagent", - }); - if (bindings.length === 0) { - return; - } - - let binding: (typeof bindings)[number] | undefined; - if (requesterThreadId) { - binding = bindings.find((entry) => { - if (entry.threadId !== requesterThreadId) { - return false; - } - if (requesterAccountId && entry.accountId !== requesterAccountId) { - return false; - } - return true; - }); - } - if (!binding && bindings.length === 1) { - binding = bindings[0]; - } - if (!binding) { - return; - } +export async function handleDiscordSubagentSpawning( + api: OpenClawPluginApi, + event: DiscordSubagentSpawningEvent, +) { + if (!event.threadRequested) { + return; + } + const channel = event.requester?.channel?.trim().toLowerCase(); + if (channel !== "discord") { + return; + } + const threadBindingFlags = resolveThreadBindingFlags(api, event.requester?.accountId); + if (!threadBindingFlags.enabled) { return { - origin: { - channel: "discord", - accountId: binding.accountId, - to: `channel:${binding.threadId}`, - threadId: binding.threadId, - }, + status: "error" as const, + error: + "Discord thread bindings are disabled (set channels.discord.threadBindings.enabled=true to override for this account, or session.threadBindings.enabled=true globally).", }; + } + if (!threadBindingFlags.spawnSubagentSessions) { + return { + status: "error" as const, + error: + "Discord thread-bound subagent spawns are disabled for this account (set channels.discord.threadBindings.spawnSubagentSessions=true to enable).", + }; + } + try { + const binding = await autoBindSpawnedDiscordSubagent({ + accountId: event.requester?.accountId, + channel: event.requester?.channel, + to: event.requester?.to, + threadId: event.requester?.threadId, + childSessionKey: event.childSessionKey, + agentId: event.agentId, + label: event.label, + boundBy: "system", + }); + if (!binding) { + return { + status: "error" as const, + error: + "Unable to create or bind a Discord thread for this subagent session. Session mode is unavailable for this target.", + }; + } + return { status: "ok" as const, threadBindingReady: true }; + } catch (err) { + return { + status: "error" as const, + error: `Discord thread bind failed: ${summarizeError(err)}`, + }; + } +} + +export function handleDiscordSubagentEnded(event: DiscordSubagentEndedEvent) { + unbindThreadBindingsBySessionKey({ + targetSessionKey: event.targetSessionKey, + accountId: event.accountId, + targetKind: event.targetKind, + reason: event.reason, + sendFarewell: event.sendFarewell, }); } + +export function handleDiscordSubagentDeliveryTarget(event: DiscordSubagentDeliveryTargetEvent) { + if (!event.expectsCompletionMessage) { + return; + } + const requesterChannel = event.requesterOrigin?.channel?.trim().toLowerCase(); + if (requesterChannel !== "discord") { + return; + } + const requesterAccountId = event.requesterOrigin?.accountId?.trim(); + const requesterThreadId = + event.requesterOrigin?.threadId != null && event.requesterOrigin.threadId !== "" + ? String(event.requesterOrigin.threadId).trim() + : ""; + const bindings = listThreadBindingsBySessionKey({ + targetSessionKey: event.childSessionKey, + ...(requesterAccountId ? { accountId: requesterAccountId } : {}), + targetKind: "subagent", + }); + if (bindings.length === 0) { + return; + } + + let binding: (typeof bindings)[number] | undefined; + if (requesterThreadId) { + binding = bindings.find((entry) => { + if (entry.threadId !== requesterThreadId) { + return false; + } + if (requesterAccountId && entry.accountId !== requesterAccountId) { + return false; + } + return true; + }); + } + if (!binding && bindings.length === 1) { + binding = bindings[0]; + } + if (!binding) { + return; + } + return { + origin: { + channel: "discord" as const, + accountId: binding.accountId, + to: `channel:${binding.threadId}`, + threadId: binding.threadId, + }, + }; +} + +export function registerDiscordSubagentHooks(api: OpenClawPluginApi) { + api.on("subagent_spawning", (event) => handleDiscordSubagentSpawning(api, event)); + api.on("subagent_ended", (event) => handleDiscordSubagentEnded(event)); + api.on("subagent_delivery_target", (event) => handleDiscordSubagentDeliveryTarget(event)); +}