fix(router): escape Discord markdown delimiters instead of stripping

Previously formatOutbound stripped `*`, `_`, `~`, `|`, `` ` ``, `#`, `>`
from prose so they wouldn't trigger Discord formatting. That worked but
silently lost information — `**DONE**`, `STEP_DONE`, ~~strike~~ all
arrived in Discord with the source characters missing.

Switch to backslash-escape so the literal source text shows up while
still suppressing the formatting interpretation. Triple-backtick fenced
code blocks are still preserved verbatim.

Escape is non-idempotent (running it twice double-escapes backslashes),
so split the pipeline:

  - sanitizeForOutbound: strip internal tags + tool-call leaks + redact
    secrets. Use this for intermediate text that will pass through
    another formatOutbound call downstream (work-item storage, channel
    wrappers, session-command output).
  - formatOutbound: sanitize + neutralizeStrayMarkdown. Reserved for the
    single Discord-send boundary in channels/discord.ts.

Internal callers (message-turn-controller, session-commands, the
sendFormattedChannelMessage / sendFormattedTrackedChannelMessage /
editFormattedTrackedChannelMessage wrappers in index.ts, and the cron
reviewer-bot path) now use sanitizeForOutbound so the markdown escape
runs exactly once at the channel boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-05-27 14:34:29 +09:00
parent 890991394d
commit cf9bf08197
5 changed files with 120 additions and 88 deletions

View File

@@ -34,7 +34,7 @@ import { startIpcWatcher } from './ipc.js';
import {
findChannel,
findChannelByName,
formatOutbound,
sanitizeForOutbound,
normalizeMessageForDedupe,
resolveChannelForDeliveryRole,
} from './router.js';
@@ -98,7 +98,7 @@ export async function sendFormattedChannelMessage(
logger.warn({ jid }, 'No channel owns JID, cannot send message');
return;
}
const text = formatOutbound(rawText);
const text = sanitizeForOutbound(rawText);
if (text) await channel.sendMessage(jid, text);
}
@@ -112,7 +112,7 @@ export async function sendFormattedTrackedChannelMessage(
logger.warn({ jid }, 'No channel owns JID, cannot send tracked message');
return null;
}
const text = formatOutbound(rawText);
const text = sanitizeForOutbound(rawText);
if (!text || !channel.sendAndTrack) return null;
return channel.sendAndTrack(jid, text);
}
@@ -128,7 +128,7 @@ export async function editFormattedTrackedChannelMessage(
logger.warn({ jid }, 'No channel owns JID, cannot edit tracked message');
return;
}
const text = formatOutbound(rawText);
const text = sanitizeForOutbound(rawText);
if (!text || !channel.editMessage) return;
await channel.editMessage(jid, messageId, text);
}
@@ -388,7 +388,7 @@ async function main(): Promise<void> {
sendFormattedChannelMessage(channels, jid, rawText),
sendMessageViaReviewerBot: reviewerChannelForCron
? async (jid, rawText) => {
const text = formatOutbound(rawText);
const text = sanitizeForOutbound(rawText);
if (text) await reviewerChannelForCron.sendMessage(jid, text);
}
: undefined,