fix(DIS-154): parse [ATTACH:] markers before sanitization
Some checks failed
CI / build-and-test (ubuntu-latest) (pull_request) Has been cancelled
CI / build-and-test (windows-latest) (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled

sanitizeForDiscord() replaced workspace paths with <disclaw> before
parseAttachments() could read them, making all resolved paths invalid.

Fix: extract attach paths from raw result.text first, then sanitize
the marker-free text. sendResponse() accepts pre-resolved paths to
skip redundant re-parsing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Tabeling 2026-04-13 10:24:38 +02:00
parent 00f7909e6b
commit 92abd97993
2 changed files with 22 additions and 9 deletions

View file

@ -67,24 +67,30 @@ export function validateAttachPath(filePath: string, workspacePath: string): str
/** /**
* Sends an agent response to a Discord channel. * Sends an agent response to a Discord channel.
* - Parses [ATTACH: path] markers and sends valid files as attachments. * - If preResolvedAttachPaths is given, those files are sent as attachments (markers
* - Text > 4000 chars (after stripping markers) is sent as a Markdown file attachment. * must already be stripped from `text`). Otherwise [ATTACH: path] markers are parsed
* from `text` on the fly (requires workspacePath).
* - Text > 4000 chars is sent as a Markdown file attachment.
* - Text <= 4000 chars is split with splitForDiscord and sent as plain messages. * - Text <= 4000 chars is split with splitForDiscord and sent as plain messages.
*/ */
export async function sendResponse( export async function sendResponse(
channel: TextChannel, channel: TextChannel,
text: string, text: string,
_triggerMessage: Message, _triggerMessage: Message,
workspacePath?: string workspacePath?: string,
preResolvedAttachPaths?: string[]
): Promise<void> { ): Promise<void> {
if (text.length === 0) { if (text.length === 0 && (!preResolvedAttachPaths || preResolvedAttachPaths.length === 0)) {
return; return;
} }
// Parse [ATTACH: ...] markers if we have a workspace context // Use pre-resolved paths (already extracted before sanitization) when available,
// otherwise fall back to parsing markers from the text (requires workspacePath).
let cleanText = text; let cleanText = text;
let attachPaths: string[] = []; let attachPaths: string[] = [];
if (workspacePath) { if (preResolvedAttachPaths) {
attachPaths = preResolvedAttachPaths;
} else if (workspacePath) {
const parsed = parseAttachments(text, workspacePath); const parsed = parseAttachments(text, workspacePath);
cleanText = parsed.text; cleanText = parsed.text;
attachPaths = parsed.attachPaths; attachPaths = parsed.attachPaths;

View file

@ -5,7 +5,7 @@ import { DisclawConfig } from "./config/loader";
import { runAgent } from "./agent/runner"; import { runAgent } from "./agent/runner";
import { sanitizeForDiscord } from "./runtime/sanitize"; import { sanitizeForDiscord } from "./runtime/sanitize";
import { ChannelQueue } from "./runtime/channel-queue"; import { ChannelQueue } from "./runtime/channel-queue";
import { sendResponse } from "./discord/send-response"; import { sendResponse, parseAttachments } from "./discord/send-response";
import { processAttachments } from "./discord/attachment-inbox"; import { processAttachments } from "./discord/attachment-inbox";
import type { RunResult } from "./agent/types"; import type { RunResult } from "./agent/types";
@ -90,7 +90,13 @@ export async function routeMessage(
message.author.username message.author.username
); );
const sanitizedResponse = sanitizeForDiscord(result.text, sanitizeOpts); // Extract [ATTACH: ...] markers BEFORE sanitization so that workspace
// paths inside the markers are still resolvable.
const { text: textWithoutMarkers, attachPaths } = parseAttachments(
result.text,
workspace.workspace_path
);
const sanitizedResponse = sanitizeForDiscord(textWithoutMarkers, sanitizeOpts);
let firstMsgId: string | null = null; let firstMsgId: string | null = null;
@ -108,7 +114,8 @@ export async function routeMessage(
// Temporarily patch channel.send for sendResponse // Temporarily patch channel.send for sendResponse
(channel as unknown as { send: typeof patchedSend }).send = patchedSend; (channel as unknown as { send: typeof patchedSend }).send = patchedSend;
try { try {
await sendResponse(channel, sanitizedResponse, message, workspace.workspace_path); // Pass pre-parsed attachPaths; sendResponse skips re-parsing when provided
await sendResponse(channel, sanitizedResponse, message, workspace.workspace_path, attachPaths);
} finally { } finally {
(channel as unknown as { send: typeof originalSend }).send = originalSend; (channel as unknown as { send: typeof originalSend }).send = originalSend;
} }