diff --git a/plugin/calendar/discord-wakeup.ts b/plugin/calendar/discord-wakeup.ts deleted file mode 100644 index 01ca1b3..0000000 --- a/plugin/calendar/discord-wakeup.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * 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; -} - -/** - * 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 { - 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 { - 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 { - 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)['__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; -} diff --git a/plugin/calendar/index.ts b/plugin/calendar/index.ts index 05eebe5..a829ccf 100644 --- a/plugin/calendar/index.ts +++ b/plugin/calendar/index.ts @@ -32,4 +32,3 @@ export * from './types'; export * from './calendar-bridge'; export * from './scheduler'; export * from './schedule-cache'; -export * from './discord-wakeup'; diff --git a/plugin/index.ts b/plugin/index.ts index 6f84171..ecc0287 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -188,52 +188,53 @@ export default { } /** - * Wake agent via Discord channel creation + message. + * Wake/spawn agent with task context for slot execution. * This is the callback invoked by CalendarScheduler when a slot is ready. - * - * Priority: - * 1. Discord wakeup (create private channel + send message) - * 2. OpenClaw spawn API (fallback if Discord not configured) */ async function wakeAgent(context: AgentWakeContext): Promise { logger.info(`Waking agent for slot: ${context.taskDescription}`); - const live = resolveConfig(); - const agentId = process.env.AGENT_ID || 'unknown'; try { - // Method 1: Discord wakeup (preferred) - const discordBotToken = (live as any).discordBotToken as string | undefined; - const discordGuildId = (live as any).discordGuildId as string | undefined; - - if (discordBotToken && discordGuildId) { - const { wakeAgentViaDiscord } = await import('./calendar/discord-wakeup.js'); - const success = await wakeAgentViaDiscord({ - botToken: discordBotToken, - guildId: discordGuildId, - agentId, - message: context.prompt, - logger, - }); - if (success) return true; - logger.warn('Discord wakeup failed, trying spawn fallback'); - } - - // Method 2: OpenClaw spawn API (fallback) + // Method 1: Use OpenClaw spawn API if available (preferred) if (api.spawn) { const result = await api.spawn({ task: context.prompt, - timeoutSeconds: context.slot.estimated_duration * 60, + timeoutSeconds: context.slot.estimated_duration * 60, // Convert to seconds }); if (result?.sessionId) { logger.info(`Agent spawned for calendar slot: session=${result.sessionId}`); + + // Track session completion trackSessionCompletion(result.sessionId, context); return true; } } - logger.warn('No wakeup method available (configure discordBotToken + discordGuildId)'); - return false; + // Method 2: Send notification/alert to wake agent (fallback) + // This relies on the agent's heartbeat to check for notifications + logger.warn('OpenClaw spawn API not available, using notification fallback'); + + // Send calendar wakeup notification via backend + const live = resolveConfig(); + const agentId = process.env.AGENT_ID || 'unknown'; + + const notifyResponse = await fetch(`${live.backendUrl}/calendar/agent/notify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Agent-ID': agentId, + 'X-Claw-Identifier': live.identifier || hostname(), + }, + body: JSON.stringify({ + agent_id: agentId, + message: context.prompt, + slot_id: context.slot.id || context.slot.virtual_id, + task_description: context.taskDescription, + }), + }); + + return notifyResponse.ok; } catch (err) { logger.error('Failed to wake agent:', err); diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index e9f0fdd..186c986 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -63,14 +63,6 @@ "managedMonitor": { "type": "string", "description": "Absolute path to an installed HarborForge.Monitor binary managed by this plugin installer. If set, gateway_start/gateway_stop hooks will start/stop the monitor process automatically." - }, - "discordBotToken": { - "type": "string", - "description": "Discord bot token for agent wakeup. Used to create private channels and send wakeup messages. Set to the same value as Dirigent moderator bot token." - }, - "discordGuildId": { - "type": "string", - "description": "Discord guild ID where wakeup channels are created." } } }