diff --git a/src/channels/discord-outbound.ts b/src/channels/discord-outbound.ts index cd6ae03..4752ba4 100644 --- a/src/channels/discord-outbound.ts +++ b/src/channels/discord-outbound.ts @@ -1,6 +1,7 @@ import path from 'path'; import { normalizeAgentOutput } from '../agent-protocol.js'; +import { neutralizeStrayMarkdown, sanitizeForOutbound } from '../router.js'; import type { OutboundAttachment } from '../types.js'; const LOCAL_MARKDOWN_LINK_RE = /\[[^\]\n]*\]\((\/[^)\n]+)\)/g; @@ -46,7 +47,17 @@ export function prepareDiscordOutbound( const structuredOutput = normalized.output?.visibility === 'public' ? normalized.output : null; const outboundText = structuredOutput?.text ?? normalized.result ?? text; - const cleanText = sanitizeLocalMarkdownLinks(outboundText); + // Redact/strip before escaping so secret patterns (which contain `_`) still + // match, then escape stray Discord markdown in free-form agent prose so + // emphasis markers (**bold**, ~~strike~~, …) render literally. Author- + // controlled structured envelopes are intentional, so their text is left + // untouched. Escaping runs before sanitizeLocalMarkdownLinks so the inline + // code it deliberately produces is preserved. + const safeText = sanitizeForOutbound(outboundText); + const renderedText = structuredOutput + ? safeText + : neutralizeStrayMarkdown(safeText); + const cleanText = sanitizeLocalMarkdownLinks(renderedText); const attachments = optionAttachments && optionAttachments.length > 0 ? optionAttachments diff --git a/src/channels/discord.ts b/src/channels/discord.ts index 8607998..54fc5b1 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -15,7 +15,7 @@ import { CACHE_DIR } from '../config.js'; import { getEnv } from '../env.js'; import { logger } from '../logger.js'; import { validateOutboundAttachments } from '../outbound-attachments.js'; -import { formatOutbound } from '../router.js'; +import { sanitizeForOutbound } from '../router.js'; import { hasReviewerLease } from '../service-routing.js'; import type { DeleteRecentMessagesByContentOptions, @@ -528,7 +528,9 @@ export class DiscordChannel implements Channel { for (const [name, id] of Object.entries(mentionMap)) { cleaned = cleaned.replace(new RegExp(`@${name}`, 'g'), `<@${id}>`); } - cleaned = formatOutbound(cleaned); + // Markdown escaping already happened in prepareDiscordOutbound (before + // the deliberate link→inline-code step). Here we only strip/redact. + cleaned = sanitizeForOutbound(cleaned); // Discord has a 2000 character limit per message and 10 attachments per message const MAX_LENGTH = 2000;