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.
* - Parses [ATTACH: path] markers and sends valid files as attachments.
* - Text > 4000 chars (after stripping markers) is sent as a Markdown file attachment.
* - If preResolvedAttachPaths is given, those files are sent as attachments (markers
* 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.
*/
export async function sendResponse(
channel: TextChannel,
text: string,
_triggerMessage: Message,
workspacePath?: string
workspacePath?: string,
preResolvedAttachPaths?: string[]
): Promise<void> {
if (text.length === 0) {
if (text.length === 0 && (!preResolvedAttachPaths || preResolvedAttachPaths.length === 0)) {
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 attachPaths: string[] = [];
if (workspacePath) {
if (preResolvedAttachPaths) {
attachPaths = preResolvedAttachPaths;
} else if (workspacePath) {
const parsed = parseAttachments(text, workspacePath);
cleanText = parsed.text;
attachPaths = parsed.attachPaths;

View file

@ -5,7 +5,7 @@ import { DisclawConfig } from "./config/loader";
import { runAgent } from "./agent/runner";
import { sanitizeForDiscord } from "./runtime/sanitize";
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 type { RunResult } from "./agent/types";
@ -90,7 +90,13 @@ export async function routeMessage(
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;
@ -108,7 +114,8 @@ export async function routeMessage(
// Temporarily patch channel.send for sendResponse
(channel as unknown as { send: typeof patchedSend }).send = patchedSend;
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 {
(channel as unknown as { send: typeof originalSend }).send = originalSend;
}