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)
This commit is contained in:
Mariusz Krawczyk 2026-01-28 15:44:53 +00:00
parent 01e0d3a320
commit ca1802015a

View File

@ -3,6 +3,7 @@ import { dispatchChannelMessageAction } from "../../channels/plugins/message-act
import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js"; import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js";
import type { MoltbotConfig } from "../../config/config.js"; import type { MoltbotConfig } from "../../config/config.js";
import { appendAssistantMessageToSessionTranscript } from "../../config/sessions.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 { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
import type { OutboundSendDeps } from "./deliver.js"; import type { OutboundSendDeps } from "./deliver.js";
import type { MessagePollResult, MessageSendResult } from "./message.js"; import type { MessagePollResult, MessageSendResult } from "./message.js";
@ -81,17 +82,58 @@ export async function executeSendAction(params: {
}> { }> {
throwIfAborted(params.ctx.abortSignal); throwIfAborted(params.ctx.abortSignal);
if (!params.ctx.dryRun) { if (!params.ctx.dryRun) {
const handled = await dispatchChannelMessageAction({ let handled: AgentToolResult<unknown> | null = null;
channel: params.ctx.channel, let pluginError: string | undefined;
action: "send",
cfg: params.ctx.cfg, try {
params: params.ctx.params, handled = await dispatchChannelMessageAction({
accountId: params.ctx.accountId ?? undefined, channel: params.ctx.channel,
gateway: params.ctx.gateway, action: "send",
toolContext: params.ctx.toolContext, cfg: params.ctx.cfg,
dryRun: params.ctx.dryRun, 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) { 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) { if (params.ctx.mirror) {
const mirrorText = params.ctx.mirror.text ?? params.message; const mirrorText = params.ctx.mirror.text ?? params.message;
const mirrorMediaUrls = const mirrorMediaUrls =
@ -114,22 +156,63 @@ export async function executeSendAction(params: {
} }
throwIfAborted(params.ctx.abortSignal); throwIfAborted(params.ctx.abortSignal);
const result: MessageSendResult = await sendMessage({
cfg: params.ctx.cfg, let result: MessageSendResult;
to: params.to, let sendError: string | undefined;
content: params.message,
mediaUrl: params.mediaUrl || undefined, try {
mediaUrls: params.mediaUrls, result = await sendMessage({
channel: params.ctx.channel || undefined, cfg: params.ctx.cfg,
accountId: params.ctx.accountId ?? undefined, to: params.to,
gifPlayback: params.gifPlayback, content: params.message,
dryRun: params.ctx.dryRun, mediaUrl: params.mediaUrl || undefined,
bestEffort: params.bestEffort ?? undefined, mediaUrls: params.mediaUrls,
deps: params.ctx.deps, channel: params.ctx.channel || undefined,
gateway: params.ctx.gateway, accountId: params.ctx.accountId ?? undefined,
mirror: params.ctx.mirror, gifPlayback: params.gifPlayback,
abortSignal: params.ctx.abortSignal, 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 { return {
handledBy: "core", handledBy: "core",