← Back to PRs

#9906: feat: wire message_sending hook in outbound delivery

by teempai open 2026-02-05 21:26 View on GitHub →
stale
## Summary Connects the existing `message_sending` plugin hook to the outbound delivery path, allowing plugins to modify outgoing message content before it is sent to channels. ## Motivation The `message_sending` hook was defined in the plugin API with full type definitions and a runner implementation (`runMessageSending` in `src/plugins/hooks.ts`), but was never called from the delivery pipeline. The sibling `message_received` hook IS wired (in `dispatch-from-config.ts`); this PR completes the pair. This enables use cases like: - **Output transformation** (e.g. decoding encoded tokens before delivery) - **Content moderation / redaction** - **Logging / auditing outbound messages** ## Changes - **`src/infra/outbound/deliver.ts`** — Call `runMessageSending` hook on each normalized payload before chunking. Only content modification is supported (returning `{ content: "..." }`). Hook runs pre-chunk intentionally so transformations apply to the full text before channel-specific splitting. - **`src/infra/outbound/deliver.ts`** — Mirror transcript now uses post-hook text so it reflects what was actually delivered, not the pre-hook original. - **`src/infra/outbound/deliver.test.ts`** — Two new tests: content modification via hook, and verification that hook is skipped when no handlers are registered. ## Design Decisions **Content-modification only (no cancel):** The hook supports `{ content: "..." }` returns but intentionally does not support cancellation. Cancel semantics in mixed-payload batches (text + media) create complex edge cases around mirror transcripts and partial delivery. Content modification covers the primary use cases. **Pre-chunk placement is intentional:** The hook runs after `normalizeReplyPayloadsForDelivery()` but before `sendTextChunks()`. This means plugins see the full text and transformations (e.g. decoding tokens) apply before the text is split into channel-sized messages. Chunks are then based on the final delivered content length. **Mirror reflects delivered content:** The mirror transcript is built from `normalizedPayloads` after hook processing, so session transcripts match what was actually sent to the channel. ## Backwards Compatibility Non-breaking. Hook only runs if plugins register handlers for `message_sending`. No existing behavior changes when no hooks are registered. ## Previous Review This addresses feedback from #9889 (closed): - ✅ Removed `cancel` support to avoid mirror/chunking edge cases - ✅ Mirror now uses post-hook text - ✅ Added comment clarifying pre-chunk placement is intentional <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR wires the existing `message_sending` plugin hook into the outbound delivery pipeline (`src/infra/outbound/deliver.ts`) so plugins can modify outgoing text before channel chunking. It also updates mirror transcript generation to reflect the post-hook text that was actually delivered, and adds unit tests covering the hook modification path and the “no handlers registered” fast-path. <h3>Confidence Score: 4/5</h3> - Mostly safe to merge once hook semantics are clarified. - Change is localized and test-covered, but the delivery path currently ignores the `cancel` field that `runMessageSending` can produce, which risks plugins believing they can cancel outbound messages when they cannot. - src/infra/outbound/deliver.ts <!-- greptile_other_comments_section --> <sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub> <!-- /greptile_comment -->

Most Similar PRs