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

@@ -4,7 +4,7 @@ import {
getAgentOutputText,
} from './agent-output.js';
import { createScopedLogger, logger } from './logger.js';
import { formatOutbound } from './router.js';
import { sanitizeForOutbound } from './router.js';
import { shouldResetSessionOnAgentFailure } from './session-recovery.js';
import { TASK_STATUS_MESSAGE_PREFIX } from './task-watch-status.js';
import { formatElapsedKorean } from './utils.js';
@@ -166,7 +166,10 @@ export class MessageTurnController {
}
const raw = getAgentOutputText(result);
const text = raw ? formatOutbound(raw) : null;
// Use sanitize (no markdown escape) — the Discord channel boundary
// applies the final escape pass, and double-escaping would produce
// visible backslash garbage.
const text = raw ? sanitizeForOutbound(raw) : null;
const attachments = getAgentOutputAttachments(result);
if (raw) {