fix(DIS-102): sanitize response before DB storage to break path feedback loop
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

The root cause was twofold:
1. Raw (unsanitized) agent responses were stored in the conversation
   history DB. On subsequent calls, these paths were fed back to the
   agent as context, causing it to reproduce full paths regardless of
   Discord-side sanitization.
2. repoRoot (workspace_path) was not passed to sanitizeForDiscord,
   so workspace-specific paths were only partially masked via the
   home directory fallback.

Fix: sanitize the full response once before both sending to Discord
and storing in the DB. Pass workspace_path as repoRoot for precise
path replacement. Add Windows path tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Tabeling 2026-04-10 10:02:17 +02:00
parent 5869c15f9d
commit 714e0c01e5
2 changed files with 38 additions and 5 deletions

View file

@ -107,30 +107,43 @@ export async function routeMessage(
message.author.username message.author.username
); );
// Sanitize the full response once, then use the sanitized version
// for both Discord output and conversation history storage.
// This prevents a feedback loop where raw paths in stored history
// get fed back to the agent as context, causing it to reproduce them.
const sanitizeOpts = {
repoRoot: workspace.workspace_path,
home: os.homedir(),
};
const sanitizedResponse = sanitizeForDiscord(response, sanitizeOpts);
// Split and send response // Split and send response
const chunks = splitMessage(response); const chunks = splitMessage(sanitizedResponse);
let firstMsgId: string | null = null; let firstMsgId: string | null = null;
for (const chunk of chunks) { for (const chunk of chunks) {
const sent = await channel.send(sanitizeForDiscord(chunk, { home: os.homedir() })); const sent = await channel.send(chunk);
if (!firstMsgId) { if (!firstMsgId) {
firstMsgId = sent.id; firstMsgId = sent.id;
} }
} }
// Store assistant response (use first message id for dedup) // Store sanitized assistant response (use first message id for dedup)
if (firstMsgId) { if (firstMsgId) {
db.addConversation( db.addConversation(
workspace.id, workspace.id,
firstMsgId, firstMsgId,
"assistant", "assistant",
response, sanitizedResponse,
null null
); );
} }
} catch (error) { } catch (error) {
const msg = error instanceof Error ? error.message : "Unbekannter Fehler"; const msg = error instanceof Error ? error.message : "Unbekannter Fehler";
const safeMsg = sanitizeForDiscord(`Fehler bei der Verarbeitung: ${msg}`, { home: os.homedir() }); const safeMsg = sanitizeForDiscord(`Fehler bei der Verarbeitung: ${msg}`, {
repoRoot: workspace.workspace_path,
home: os.homedir(),
});
await channel.send(safeMsg).catch(() => {}); await channel.send(safeMsg).catch(() => {});
} finally { } finally {
clearInterval(typingInterval); clearInterval(typingInterval);

View file

@ -64,4 +64,24 @@ describe("sanitizeForDiscord", () => {
}); });
expect(result).toBe("<disclaw>/src/index.ts and ~/other.ts"); expect(result).toBe("<disclaw>/src/index.ts and ~/other.ts");
}); });
// Windows-style backslash paths are sanitized
it("replaces Windows backslash paths", () => {
const text = "File at C:\\Users\\dev\\.disclaw\\workspaces\\agent-x\\src\\main.ts";
const result = sanitizeForDiscord(text, {
repoRoot: "C:\\Users\\dev\\.disclaw\\workspaces\\agent-x",
home: "C:\\Users\\dev",
});
expect(result).toBe("File at <disclaw>\\src\\main.ts");
});
// Mixed slash styles (common on Windows when tools output forward slashes)
it("replaces forward-slash variants of Windows paths", () => {
const text = "File at C:/Users/dev/.disclaw/workspaces/agent-x/src/main.ts";
const result = sanitizeForDiscord(text, {
repoRoot: "C:\\Users\\dev\\.disclaw\\workspaces\\agent-x",
home: "C:\\Users\\dev",
});
expect(result).toBe("File at <disclaw>/src/main.ts");
});
}); });