diff --git a/src/discord/send-response.ts b/src/discord/send-response.ts index e6937ac..88cadf3 100644 --- a/src/discord/send-response.ts +++ b/src/discord/send-response.ts @@ -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 { - 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; diff --git a/src/router.ts b/src/router.ts index bd63d6b..a3d7ea2 100644 --- a/src/router.ts +++ b/src/router.ts @@ -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; }