From c89242b6dd286c5d6f084462d9a26d91448bda60 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Wed, 25 Mar 2026 18:06:31 +0900 Subject: [PATCH] fix: harden stripToolCallLeaks regex for CJK/non-ASCII garbage tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous regex used \w+ for descriptor tokens between the function name and JSON body, which only matches ASCII word characters. Model hallucinations often inject CJK/non-ASCII text (e.g. 彩神争霸) in this position. Switch to (?:\s+[^\s{}]+)+ to match any whitespace-separated non-brace tokens regardless of character set. Also removes stale backward-compat re-exports from index.ts and updates the test import for composeDashboardContent. Co-Authored-By: Claude Opus 4.6 --- src/formatting.test.ts | 18 ++++++++++++++++++ src/index.test.ts | 2 +- src/index.ts | 4 ---- src/router.ts | 10 +++++++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/formatting.test.ts b/src/formatting.test.ts index 33cd0e9..ad24eec 100644 --- a/src/formatting.test.ts +++ b/src/formatting.test.ts @@ -272,6 +272,24 @@ describe('stripToolCallLeaks', () => { it('returns empty string for empty input', () => { expect(stripToolCallLeaks('')).toBe(''); }); + + it('strips tool-call leaks with CJK/non-ASCII garbage between tokens', () => { + const cjkLeak = + 'to=functions.exec_command 彩神争霸大发快三 json code {"cmd":"printf \'noop\\n\'","yield_time_ms":1000,"max_output_tokens":20}'; + expect(stripToolCallLeaks(cjkLeak)).toBe(''); + }); + + it('strips tool-call leaks with mixed non-ASCII and preserves surrounding text', () => { + const mixed = + '대기 중입니다.to=functions.exec_command 彩神争霸 json code {"cmd":"ls"}\n다음 작업'; + expect(stripToolCallLeaks(mixed)).toBe('대기 중입니다.\n다음 작업'); + }); + + it('strips tool-call leaks with multiple non-ASCII words before JSON', () => { + const leak = + 'to=functions.read_file données café résumé code {"path":"/tmp/test"}'; + expect(stripToolCallLeaks(leak)).toBe(''); + }); }); describe('formatOutbound with tool-call leaks', () => { diff --git a/src/index.test.ts b/src/index.test.ts index fa19cb7..ef77dff 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it, vi } from 'vitest'; import { - composeDashboardContent, editFormattedTrackedChannelMessage, sendFormattedChannelMessage, sendFormattedTrackedChannelMessage, } from './index.js'; +import { composeDashboardContent } from './dashboard-render.js'; import { Channel } from './types.js'; function makeChannel(overrides: Partial = {}): Channel { diff --git a/src/index.ts b/src/index.ts index 23d0535..4d9265a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,10 +70,6 @@ import { stopTokenRefreshLoop, } from './token-refresh.js'; -// Re-export for backwards compatibility during refactor -export { escapeXml, formatMessages } from './router.js'; -export { composeDashboardContent } from './dashboard-render.js'; - // Token rotation is initialized lazily on first use or at startup below export async function sendFormattedChannelMessage( diff --git a/src/router.ts b/src/router.ts index 21dcead..2c606af 100644 --- a/src/router.ts +++ b/src/router.ts @@ -58,16 +58,20 @@ function redactSecrets(text: string): string { * * When a model (especially Codex) enters a degenerate loop, it emits * tool-call intent as plaintext instead of actual tool calls. The format is: - * to=functions. {} + * to=functions. {} * e.g. `to=functions.exec_command code {"cmd":"git status","yield_time_ms":1000}` * + * The tokens between the function name and JSON body can include non-ASCII + * characters (CJK, etc.) when the model hallucinates. The regex allows one or + * more non-whitespace descriptor tokens before the JSON brace. + * * This function removes such fragments so they never reach Discord. */ export function stripToolCallLeaks(text: string): string { - // Match tool-call serialization: to=functions. {} + // Match tool-call serialization: to=functions. {} // Handles up to one level of nested braces in the JSON body. const stripped = text.replace( - /to=functions\.\w+\s+\w+\s+\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g, + /to=functions\.\w+(?:\s+[^\s{}]+)+\s+\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g, '', ); // Collapse excessive blank lines left after stripping