From a442976d773bfffe5686a1dcb76d16a1c8d7bbde Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 20:44:57 +0900 Subject: [PATCH] Fix invisible characters in agent names --- src/shared/agent-display-names.test.ts | 9 ++++++++ src/shared/agent-display-names.ts | 14 +++++++++---- .../call-omo-agent/sync-executor.test.ts | 21 +++++++++++++++++++ .../subagent-resolver.test.ts | 15 +++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/shared/agent-display-names.test.ts b/src/shared/agent-display-names.test.ts index 353bfb31e..57cdd3c27 100644 --- a/src/shared/agent-display-names.test.ts +++ b/src/shared/agent-display-names.test.ts @@ -186,6 +186,11 @@ describe("getAgentConfigKey", () => { it("resolves atlas even when the UI ordering prefix is present", () => { expect(getAgentConfigKey(getAgentListDisplayName("atlas"))).toBe("atlas") }) + + it("resolves display names even when zero-width characters are embedded", () => { + expect(getAgentConfigKey("Sisyphus\u200B - Ultraworker")).toBe("sisyphus") + expect(getAgentConfigKey("\uFEFFAtlas - Plan Executor")).toBe("atlas") + }) }) describe("getAgentListDisplayName", () => { @@ -208,6 +213,10 @@ describe("normalizeAgentForPrompt", () => { expect(normalizeAgentForPrompt(getAgentListDisplayName("prometheus"))).toBe("Prometheus - Plan Builder") expect(normalizeAgentForPrompt(getAgentListDisplayName("atlas"))).toBe("Atlas - Plan Executor") }) + + it("removes zero-width characters before returning canonical names", () => { + expect(normalizeAgentForPrompt("Sisyphus\u200B - Ultraworker")).toBe("Sisyphus - Ultraworker") + }) }) describe("normalizeAgentForPromptKey", () => { diff --git a/src/shared/agent-display-names.ts b/src/shared/agent-display-names.ts index 6070393e4..677ab13d9 100644 --- a/src/shared/agent-display-names.ts +++ b/src/shared/agent-display-names.ts @@ -33,8 +33,14 @@ const AGENT_LIST_SORT_PREFIXES: Record = { atlas: "\u200B\u200B\u200B\u200B", } +const ZERO_WIDTH_AGENT_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g + +export function stripInvisibleAgentCharacters(agentName: string): string { + return agentName.replace(ZERO_WIDTH_AGENT_CHARACTERS_REGEX, "") +} + export function stripAgentListSortPrefix(agentName: string): string { - return agentName.replace(/^\u200B+/, "") + return stripInvisibleAgentCharacters(agentName) } export function getAgentRuntimeName(configKey: string): string { @@ -97,7 +103,7 @@ const LEGACY_DISPLAY_NAMES: Record = { * "Atlas - Plan Executor" -> "atlas", "Atlas (Plan Executor)" -> "atlas", "atlas" -> "atlas" */ export function getAgentConfigKey(agentName: string): string { - const lower = stripAgentListSortPrefix(agentName).toLowerCase() + const lower = stripAgentListSortPrefix(agentName).trim().toLowerCase() const reversed = REVERSE_DISPLAY_NAMES[lower] if (reversed !== undefined) return reversed const legacy = LEGACY_DISPLAY_NAMES[lower] @@ -117,7 +123,7 @@ export function normalizeAgentForPrompt(agentName: string | undefined): string | return undefined } - const trimmed = stripAgentListSortPrefix(agentName.trim()) + const trimmed = stripAgentListSortPrefix(agentName).trim() if (!trimmed) { return undefined } @@ -143,7 +149,7 @@ export function normalizeAgentForPromptKey(agentName: string | undefined): strin return undefined } - const trimmed = stripAgentListSortPrefix(agentName.trim()) + const trimmed = stripAgentListSortPrefix(agentName).trim() if (!trimmed) { return undefined } diff --git a/src/tools/call-omo-agent/sync-executor.test.ts b/src/tools/call-omo-agent/sync-executor.test.ts index 4100a4da9..18f1147f2 100644 --- a/src/tools/call-omo-agent/sync-executor.test.ts +++ b/src/tools/call-omo-agent/sync-executor.test.ts @@ -115,6 +115,27 @@ describe("executeSync", () => { expect(promptInput?.body.parts).toEqual([{ type: "text", text: "find something" }]) }) + test("removes invisible agent characters before sending the sync prompt", async () => { + //#given + const executeSync = await importExecuteSync() + const deps = createDependencies() + const toolContext = createToolContext() + const recorder = createPromptAsyncRecorder() + const args = { + subagent_type: "\u200BSisyphus\u200B - Ultraworker", + description: "test task", + prompt: "find something", + run_in_background: false, + } + + //#when + await executeSync(args, toolContext, createContext(recorder.promptAsync) as never, deps) + + //#then + const promptInput = recorder.getCapturedInput() + expect(promptInput?.body.agent).toBe("Sisyphus - Ultraworker") + }) + test("returns processed response with task metadata footer", async () => { //#given const executeSync = await importExecuteSync() diff --git a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts index 11684a4b3..4f84fc01f 100644 --- a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts +++ b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts @@ -163,6 +163,21 @@ describe("resolveSubagentExecution", () => { expect(result.categoryModel).toEqual({ providerID: "openai", modelID: "gpt-5.3-codex" }) }) + test("matches agents even when zero-width characters are present in the requested name", async () => { + //#given + const args = createBaseArgs({ subagent_type: "\uFEFFSisyphus - Ultraworker" }) + const executorCtx = createExecutorContext(async () => ([ + { name: "\u200BSisyphus - Ultraworker", mode: "subagent", model: "openai/gpt-5.3-codex" }, + ])) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "oracle", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.agentToUse).toBe("Sisyphus - Ultraworker") + }) + test("uses agent override fallback_models for subagent runtime fallback chain", async () => { //#given readProviderModelsCacheMock.mockReturnValue({