feat: Discord-based agent wakeup replacing spawn

New wakeup flow:
1. Create private Discord channel for the agent
2. Send wakeup message with slot context + workflow reference
3. If Dirigent detected (globalThis.__dirigent), create work-type channel
4. Fallback to api.spawn if Discord not configured

New config fields: discordBotToken, discordGuildId
New file: plugin/calendar/discord-wakeup.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
operator
2026-04-18 20:28:59 +00:00
parent 4a66a7cc90
commit 86c9d43c97
4 changed files with 209 additions and 29 deletions

View File

@@ -0,0 +1,172 @@
/**
* Discord-based agent wakeup: create a private channel and send a wakeup message.
*
* If Dirigent is detected (via globalThis.__dirigent), creates a work-type channel.
* Otherwise, creates a plain private Discord channel.
*/
const DISCORD_API = 'https://discord.com/api/v10';
interface WakeupConfig {
botToken: string;
guildId: string;
agentDiscordId?: string;
agentId: string;
message: string;
logger: {
info: (...args: any[]) => void;
warn: (...args: any[]) => void;
error: (...args: any[]) => void;
};
}
interface DirigentApi {
createWorkChannel?: (params: {
guildId: string;
name: string;
agentDiscordId: string;
}) => Promise<string>;
}
/**
* Get bot user ID from token (decode JWT-like Discord token).
*/
function getBotUserIdFromToken(token: string): string | null {
try {
const base64 = token.split('.')[0];
const decoded = Buffer.from(base64, 'base64').toString('utf8');
return decoded || null;
} catch {
return null;
}
}
/**
* Create a private Discord channel visible only to the target agent and bot.
*/
async function createPrivateChannel(
token: string,
guildId: string,
name: string,
memberIds: string[],
logger: WakeupConfig['logger']
): Promise<string | null> {
const botId = getBotUserIdFromToken(token);
// Permission overwrites: deny @everyone, allow specific members
const permissionOverwrites = [
{ id: guildId, type: 0, deny: '1024' }, // deny @everyone view
...memberIds.map(id => ({ id, type: 1, allow: '1024' })), // allow members view
...(botId ? [{ id: botId, type: 1, allow: '1024' }] : []),
];
try {
const res = await fetch(`${DISCORD_API}/guilds/${guildId}/channels`, {
method: 'POST',
headers: {
'Authorization': `Bot ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
type: 0, // text channel
permission_overwrites: permissionOverwrites,
}),
});
if (!res.ok) {
logger.warn(`Discord channel creation failed: ${res.status} ${await res.text()}`);
return null;
}
const data = await res.json() as { id: string };
return data.id;
} catch (err) {
logger.error(`Discord channel creation error: ${String(err)}`);
return null;
}
}
/**
* Send a message to a Discord channel.
*/
async function sendMessage(
token: string,
channelId: string,
content: string,
logger: WakeupConfig['logger']
): Promise<boolean> {
try {
const res = await fetch(`${DISCORD_API}/channels/${channelId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bot ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ content }),
});
if (!res.ok) {
logger.warn(`Discord message send failed: ${res.status}`);
return false;
}
return true;
} catch (err) {
logger.error(`Discord message send error: ${String(err)}`);
return false;
}
}
/**
* Wake an agent via Discord: create a private channel and send the wakeup message.
*/
export async function wakeAgentViaDiscord(config: WakeupConfig): Promise<boolean> {
const { botToken, guildId, agentDiscordId, agentId, message, logger } = config;
if (!botToken || !guildId) {
logger.warn('Discord wakeup: botToken or guildId not configured');
return false;
}
// Check if Dirigent is available for work channel creation
const dirigent = (globalThis as Record<string, unknown>)['__dirigent'] as DirigentApi | undefined;
let channelId: string | null = null;
const channelName = `hf-wakeup-${agentId}-${Date.now()}`;
if (dirigent?.createWorkChannel && agentDiscordId) {
// Use Dirigent to create a work-type channel (with turn management)
try {
channelId = await dirigent.createWorkChannel({
guildId,
name: channelName,
agentDiscordId,
});
logger.info(`Wakeup channel created via Dirigent: ${channelId}`);
} catch (err) {
logger.warn(`Dirigent work channel creation failed, falling back to plain channel: ${String(err)}`);
}
}
if (!channelId) {
// Fallback: create a plain private Discord channel
const memberIds = agentDiscordId ? [agentDiscordId] : [];
channelId = await createPrivateChannel(botToken, guildId, channelName, memberIds, logger);
if (channelId) {
logger.info(`Wakeup channel created (plain): ${channelId}`);
}
}
if (!channelId) {
logger.error('Failed to create wakeup channel');
return false;
}
// Send the wakeup message
const sent = await sendMessage(botToken, channelId, message, logger);
if (sent) {
logger.info(`Wakeup message sent to ${channelId} for agent ${agentId}`);
}
return sent;
}