From ca1802015a6ca294e55296c625031c2e31b7b593 Mon Sep 17 00:00:00 2001 From: Mariusz Krawczyk Date: Wed, 28 Jan 2026 15:44:53 +0000 Subject: [PATCH] feat(hooks): wire up message_sent plugin hook Fire the message_sent hook after successful/failed message sends. This enables plugins to implement retry logic, logging, or other post-send behaviors. The hook is called in executeSendAction for both: - Plugin-handled sends (via dispatchChannelMessageAction) - Core sends (via sendMessage) Hook payload includes: - to: recipient - content: message text - success: boolean - error: error message (when success=false) --- src/infra/outbound/outbound-send-service.ts | 135 ++++++++++++++++---- 1 file changed, 109 insertions(+), 26 deletions(-) diff --git a/src/infra/outbound/outbound-send-service.ts b/src/infra/outbound/outbound-send-service.ts index 6499eb452..2938aad84 100644 --- a/src/infra/outbound/outbound-send-service.ts +++ b/src/infra/outbound/outbound-send-service.ts @@ -3,6 +3,7 @@ import { dispatchChannelMessageAction } from "../../channels/plugins/message-act import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js"; import type { MoltbotConfig } from "../../config/config.js"; import { appendAssistantMessageToSessionTranscript } from "../../config/sessions.js"; +import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js"; import type { OutboundSendDeps } from "./deliver.js"; import type { MessagePollResult, MessageSendResult } from "./message.js"; @@ -81,17 +82,58 @@ export async function executeSendAction(params: { }> { throwIfAborted(params.ctx.abortSignal); if (!params.ctx.dryRun) { - const handled = await dispatchChannelMessageAction({ - channel: params.ctx.channel, - action: "send", - cfg: params.ctx.cfg, - params: params.ctx.params, - accountId: params.ctx.accountId ?? undefined, - gateway: params.ctx.gateway, - toolContext: params.ctx.toolContext, - dryRun: params.ctx.dryRun, - }); + let handled: AgentToolResult | null = null; + let pluginError: string | undefined; + + try { + handled = await dispatchChannelMessageAction({ + channel: params.ctx.channel, + action: "send", + cfg: params.ctx.cfg, + params: params.ctx.params, + accountId: params.ctx.accountId ?? undefined, + gateway: params.ctx.gateway, + toolContext: params.ctx.toolContext, + dryRun: params.ctx.dryRun, + }); + } catch (err) { + pluginError = err instanceof Error ? err.message : String(err); + // Fire hook for failed plugin send + const hookRunner = getGlobalHookRunner(); + if (hookRunner) { + hookRunner.runMessageSent( + { + to: params.to, + content: params.message, + success: false, + error: pluginError, + }, + { + channelId: params.ctx.channel, + accountId: params.ctx.accountId ?? undefined, + }, + ); + } + throw err; + } + if (handled) { + // Fire hook for successful plugin send + const hookRunner = getGlobalHookRunner(); + if (hookRunner) { + hookRunner.runMessageSent( + { + to: params.to, + content: params.message, + success: true, + }, + { + channelId: params.ctx.channel, + accountId: params.ctx.accountId ?? undefined, + }, + ); + } + if (params.ctx.mirror) { const mirrorText = params.ctx.mirror.text ?? params.message; const mirrorMediaUrls = @@ -114,22 +156,63 @@ export async function executeSendAction(params: { } throwIfAborted(params.ctx.abortSignal); - const result: MessageSendResult = await sendMessage({ - cfg: params.ctx.cfg, - to: params.to, - content: params.message, - mediaUrl: params.mediaUrl || undefined, - mediaUrls: params.mediaUrls, - channel: params.ctx.channel || undefined, - accountId: params.ctx.accountId ?? undefined, - gifPlayback: params.gifPlayback, - dryRun: params.ctx.dryRun, - bestEffort: params.bestEffort ?? undefined, - deps: params.ctx.deps, - gateway: params.ctx.gateway, - mirror: params.ctx.mirror, - abortSignal: params.ctx.abortSignal, - }); + + let result: MessageSendResult; + let sendError: string | undefined; + + try { + result = await sendMessage({ + cfg: params.ctx.cfg, + to: params.to, + content: params.message, + mediaUrl: params.mediaUrl || undefined, + mediaUrls: params.mediaUrls, + channel: params.ctx.channel || undefined, + accountId: params.ctx.accountId ?? undefined, + gifPlayback: params.gifPlayback, + dryRun: params.ctx.dryRun, + bestEffort: params.bestEffort ?? undefined, + deps: params.ctx.deps, + gateway: params.ctx.gateway, + mirror: params.ctx.mirror, + abortSignal: params.ctx.abortSignal, + }); + } catch (err) { + sendError = err instanceof Error ? err.message : String(err); + // Fire hook for failed send + const hookRunner = getGlobalHookRunner(); + if (hookRunner) { + hookRunner.runMessageSent( + { + to: params.to, + content: params.message, + success: false, + error: sendError, + }, + { + channelId: params.ctx.channel, + accountId: params.ctx.accountId ?? undefined, + }, + ); + } + throw err; + } + + // Fire hook for successful send + const hookRunner = getGlobalHookRunner(); + if (hookRunner) { + hookRunner.runMessageSent( + { + to: params.to, + content: params.message, + success: true, + }, + { + channelId: params.ctx.channel, + accountId: params.ctx.accountId ?? undefined, + }, + ); + } return { handledBy: "core",