refactor(discord): lazy-load subagent hooks

This commit is contained in:
Vincent Koc 2026-04-03 23:22:26 +09:00
parent 0ecf84524d
commit 690c58baa2
2 changed files with 185 additions and 128 deletions

View File

@ -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<DiscordSubagentHooksModule> | 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);
});
},
});

View File

@ -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));
}