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