From 333ad3aaddbe0d43943d70909b5b2d63e6c6e344 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 17:36:05 +0900 Subject: [PATCH] refactor(agents): drop ZWSP prefixes from agent display names The sort shim from the previous commit enforces canonical core ordering at runtime, so ZWSP prefixes are no longer needed. Removing them eliminates the Bun.stringWidth vs terminal-width drift that broke the TUI status bar (#3259). Drop AGENT_LIST_SORT_PREFIXES and getAgentRuntimeName from agent-display-names; switch all call sites to getAgentDisplayName. getAgentListDisplayName stays as a thin alias for external importers. Keep stripInvisibleAgentCharacters and the ZWSP regex paths so legacy session state and configs from v3.14.0-v3.16.0 still resolve. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/cli/run/agent-resolver.ts | 8 ++-- src/cli/run/runner.test.ts | 14 +++--- .../claude-code-session-state/state.test.ts | 14 ++++++ .../agent-config-handler.test.ts | 32 ++++++++++--- src/plugin-handlers/agent-config-handler.ts | 6 +-- .../agent-key-remapper.test.ts | 35 ++++++++++---- src/plugin-handlers/agent-key-remapper.ts | 4 +- src/plugin-handlers/config-handler.test.ts | 24 +++++----- src/plugin-interface.test.ts | 48 ++++++++++++++++++- src/plugin/chat-message.test.ts | 14 ++++++ src/shared/agent-display-names.test.ts | 24 +++++++--- src/shared/agent-display-names.ts | 28 ++++------- .../subagent-resolver.test.ts | 20 ++++++++ 13 files changed, 201 insertions(+), 70 deletions(-) diff --git a/src/cli/run/agent-resolver.ts b/src/cli/run/agent-resolver.ts index b9dd27a64..5f2047eb4 100644 --- a/src/cli/run/agent-resolver.ts +++ b/src/cli/run/agent-resolver.ts @@ -1,7 +1,7 @@ import pc from "picocolors" import type { RunOptions } from "./types" import type { OhMyOpenCodeConfig } from "../../config" -import { getAgentConfigKey, getAgentDisplayName, getAgentRuntimeName } from "../../shared/agent-display-names" +import { getAgentConfigKey, getAgentDisplayName } from "../../shared/agent-display-names" const CORE_AGENT_ORDER = ["sisyphus", "hephaestus", "prometheus", "atlas"] as const const DEFAULT_AGENT = "sisyphus" @@ -21,7 +21,7 @@ const normalizeAgentName = (agent?: string): ResolvedAgent | undefined => { const configKey = getAgentConfigKey(trimmed) const displayName = getAgentDisplayName(configKey) - const runtimeName = getAgentRuntimeName(configKey) + const runtimeName = getAgentDisplayName(configKey) const isKnownAgent = displayName !== configKey return { @@ -62,13 +62,13 @@ export const resolveRunAgent = ( envAgent ?? configAgent ?? { configKey: DEFAULT_AGENT, - resolvedName: getAgentRuntimeName(DEFAULT_AGENT), + resolvedName: getAgentDisplayName(DEFAULT_AGENT), } if (isAgentDisabled(resolved.configKey, pluginConfig)) { const fallback = pickFallbackAgent(pluginConfig) const fallbackDisplayName = getAgentDisplayName(fallback) - const fallbackRuntimeName = getAgentRuntimeName(fallback) + const fallbackRuntimeName = getAgentDisplayName(fallback) const fallbackDisabled = isAgentDisabled(fallback, pluginConfig) if (fallbackDisabled) { console.log( diff --git a/src/cli/run/runner.test.ts b/src/cli/run/runner.test.ts index 12a52bf15..e131c536a 100644 --- a/src/cli/run/runner.test.ts +++ b/src/cli/run/runner.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test" import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "../../config" import { resolveRunAgent } from "./agent-resolver" -import { getAgentRuntimeName } from "../../shared/agent-display-names" +import { getAgentDisplayName } from "../../shared/agent-display-names" const createConfig = (overrides: Partial = {}): OhMyOpenCodeConfig => OhMyOpenCodeConfigSchema.parse(overrides) @@ -32,7 +32,7 @@ describe("resolveRunAgent", () => { ) // then - expect(agent).toBe(getAgentRuntimeName("hephaestus")) + expect(agent).toBe(getAgentDisplayName("hephaestus")) }) it("uses env agent over config", () => { @@ -44,7 +44,7 @@ describe("resolveRunAgent", () => { const agent = resolveRunAgent({ message: "test" }, config, env) // then - expect(agent).toBe(getAgentRuntimeName("atlas")) + expect(agent).toBe(getAgentDisplayName("atlas")) }) it("uses config agent over default", () => { @@ -55,7 +55,7 @@ describe("resolveRunAgent", () => { const agent = resolveRunAgent({ message: "test" }, config, {}) // then - expect(agent).toBe(getAgentRuntimeName("prometheus")) + expect(agent).toBe(getAgentDisplayName("prometheus")) }) it("falls back to sisyphus when none set", () => { @@ -66,7 +66,7 @@ describe("resolveRunAgent", () => { const agent = resolveRunAgent({ message: "test" }, config, {}) // then - expect(agent).toBe(getAgentRuntimeName("sisyphus")) + expect(agent).toBe(getAgentDisplayName("sisyphus")) }) it("skips disabled sisyphus for next available core agent", () => { @@ -77,7 +77,7 @@ describe("resolveRunAgent", () => { const agent = resolveRunAgent({ message: "test" }, config, {}) // then - expect(agent).toBe(getAgentRuntimeName("hephaestus")) + expect(agent).toBe(getAgentDisplayName("hephaestus")) }) it("maps display-name style default_run_agent values to canonical runtime names", () => { @@ -88,7 +88,7 @@ describe("resolveRunAgent", () => { const agent = resolveRunAgent({ message: "test" }, config, {}) // then - expect(agent).toBe(getAgentRuntimeName("sisyphus")) + expect(agent).toBe(getAgentDisplayName("sisyphus")) }) }) diff --git a/src/features/claude-code-session-state/state.test.ts b/src/features/claude-code-session-state/state.test.ts index c6898adbf..fd5de17c4 100644 --- a/src/features/claude-code-session-state/state.test.ts +++ b/src/features/claude-code-session-state/state.test.ts @@ -247,4 +247,18 @@ describe("claude-code-session-state", () => { expect(getSessionAgent(sessionID)).toBe(newAgent) }) }) + + describe("backward compatibility", () => { + test("strips legacy ZWSP-prefixed agent names from persisted session state (GH-3259)", () => { + // given - persisted session payload from v3.14.0-v3.16.0 with ZWSP prefix + const sessionID = "test-session-legacy-zwsp" + const legacyAgent = "\u200B\u200BHephaestus - Deep Agent" + + // when + setSessionAgent(sessionID, legacyAgent) + + // then + expect(getSessionAgent(sessionID)).toBe("Hephaestus - Deep Agent") + }) + }) }) diff --git a/src/plugin-handlers/agent-config-handler.test.ts b/src/plugin-handlers/agent-config-handler.test.ts index 74ea03c8a..69d05ef1b 100644 --- a/src/plugin-handlers/agent-config-handler.test.ts +++ b/src/plugin-handlers/agent-config-handler.test.ts @@ -9,7 +9,7 @@ import type { OhMyOpenCodeConfig } from "../config" import * as agentLoader from "../features/claude-code-agent-loader" import * as skillLoader from "../features/opencode-skill-loader" import type { LoadedSkill } from "../features/opencode-skill-loader" -import { getAgentListDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" +import { getAgentDisplayName, getAgentListDisplayName } from "../shared/agent-display-names" import { applyAgentConfig } from "./agent-config-handler" import type { PluginComponents } from "./plugin-components-loader" @@ -205,7 +205,7 @@ describe("applyAgentConfig builtin override protection", () => { }) // then - expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) + expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) }) test("keeps config-key default_agent behavior unchanged", async () => { @@ -222,7 +222,7 @@ describe("applyAgentConfig builtin override protection", () => { }) // then - expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) + expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) }) test("keeps fallback default_agent behavior unchanged", async () => { @@ -238,7 +238,25 @@ describe("applyAgentConfig builtin override protection", () => { }) // then - expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) + expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) + }) + + test("resolved default_agent contains no zero-width invisible characters", async () => { + // given canonical core ordering is now enforced by the agent sort shim, so + // default_agent must not carry the legacy ZWSP prefix that earlier biased + // OpenCode's localeCompare sort. + const config = createBaseConfig() + + // when applyAgentConfig resolves the default agent + await applyAgentConfig({ + config, + pluginConfig: createPluginConfig(), + ctx: { directory: "/tmp" }, + pluginComponents: createPluginComponents(), + }) + + // then the persisted default_agent is the clean display name + expect(config.default_agent).not.toMatch(/[\u200B\u200C\u200D\uFEFF]/) }) test("filters user agents whose key matches the builtin display-name alias", async () => { @@ -262,7 +280,7 @@ describe("applyAgentConfig builtin override protection", () => { // then expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual({ ...builtinSisyphusConfig, - name: getAgentRuntimeName("sisyphus"), + name: getAgentDisplayName("sisyphus"), }) }) @@ -287,7 +305,7 @@ describe("applyAgentConfig builtin override protection", () => { // then expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual({ ...builtinSisyphusConfig, - name: getAgentRuntimeName("sisyphus"), + name: getAgentDisplayName("sisyphus"), }) expect(result.SiSyPhUs).toBeUndefined() }) @@ -314,7 +332,7 @@ describe("applyAgentConfig builtin override protection", () => { // then expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual({ ...builtinSisyphusConfig, - name: getAgentRuntimeName("sisyphus"), + name: getAgentDisplayName("sisyphus"), }) }) diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index cc29d71c8..cbcbdd647 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -4,7 +4,7 @@ import type { OhMyOpenCodeConfig } from "../config"; import { isTaskSystemEnabled, log, migrateAgentConfig } from "../shared"; import { getAgentConfigKey, - getAgentRuntimeName, + getAgentDisplayName, normalizeAgentForPromptKey, } from "../shared/agent-display-names"; import { AGENT_NAME_MAP } from "../shared/migration"; @@ -196,10 +196,10 @@ export async function applyAgentConfig(params: { const configKey = getAgentConfigKey(configuredDefaultAgent); const runtimeConfigKey = normalizeAgentForPromptKey(configuredDefaultAgent) ?? configKey; (params.config as { default_agent?: string }).default_agent = - getAgentRuntimeName(runtimeConfigKey); + getAgentDisplayName(runtimeConfigKey); } else { (params.config as { default_agent?: string }).default_agent = - getAgentRuntimeName("sisyphus"); + getAgentDisplayName("sisyphus"); } // Assembly order: Sisyphus -> Hephaestus -> Prometheus -> Atlas diff --git a/src/plugin-handlers/agent-key-remapper.test.ts b/src/plugin-handlers/agent-key-remapper.test.ts index 2153890c7..7c4ff25e4 100644 --- a/src/plugin-handlers/agent-key-remapper.test.ts +++ b/src/plugin-handlers/agent-key-remapper.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "bun:test" import { remapAgentKeysToDisplayNames } from "./agent-key-remapper" -import { getAgentDisplayName, getAgentListDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" +import { getAgentDisplayName, getAgentListDisplayName } from "../shared/agent-display-names" describe("remapAgentKeysToDisplayNames", () => { it("remaps known agent keys to display names", () => { @@ -124,22 +124,22 @@ describe("remapAgentKeysToDisplayNames", () => { getAgentListDisplayName("atlas"), ]) expect(result[getAgentListDisplayName("sisyphus")]).toEqual({ - name: getAgentRuntimeName("sisyphus"), + name: getAgentListDisplayName("sisyphus"), prompt: "test", mode: "primary", }) expect(result[getAgentListDisplayName("hephaestus")]).toEqual({ - name: getAgentRuntimeName("hephaestus"), + name: getAgentListDisplayName("hephaestus"), prompt: "test", mode: "primary", }) expect(result[getAgentListDisplayName("prometheus")]).toEqual({ - name: getAgentRuntimeName("prometheus"), + name: getAgentListDisplayName("prometheus"), prompt: "test", mode: "primary", }) expect(result[getAgentListDisplayName("atlas")]).toEqual({ - name: getAgentRuntimeName("atlas"), + name: getAgentListDisplayName("atlas"), prompt: "test", mode: "primary", }) @@ -160,24 +160,41 @@ describe("remapAgentKeysToDisplayNames", () => { // then runtime-facing names stay aligned even when builtin configs omit name expect(result[getAgentListDisplayName("sisyphus")]).toEqual({ - name: getAgentRuntimeName("sisyphus"), + name: getAgentListDisplayName("sisyphus"), prompt: "test", mode: "primary", }) expect(result[getAgentListDisplayName("hephaestus")]).toEqual({ - name: getAgentRuntimeName("hephaestus"), + name: getAgentListDisplayName("hephaestus"), prompt: "test", mode: "primary", }) expect(result[getAgentListDisplayName("prometheus")]).toEqual({ - name: getAgentRuntimeName("prometheus"), + name: getAgentListDisplayName("prometheus"), prompt: "test", mode: "primary", }) expect(result[getAgentListDisplayName("atlas")]).toEqual({ - name: getAgentRuntimeName("atlas"), + name: getAgentListDisplayName("atlas"), prompt: "test", mode: "primary", }) }) + + it("emits a single literal display-name row with no ZWSP for a single core agent", () => { + // given a single core agent input + const agents = { + sisyphus: { foo: "bar" }, + } + + // when remapping + const result = remapAgentKeysToDisplayNames(agents) + + // then exactly one row is emitted under the clean literal display name + expect(Object.keys(result)).toEqual(["Sisyphus - Ultraworker"]) + expect(result["Sisyphus - Ultraworker"]).toEqual({ + name: "Sisyphus - Ultraworker", + foo: "bar", + }) + }) }) diff --git a/src/plugin-handlers/agent-key-remapper.ts b/src/plugin-handlers/agent-key-remapper.ts index 56aea9ae9..e75ab21b3 100644 --- a/src/plugin-handlers/agent-key-remapper.ts +++ b/src/plugin-handlers/agent-key-remapper.ts @@ -1,4 +1,4 @@ -import { getAgentListDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" +import { getAgentListDisplayName } from "../shared/agent-display-names" function rewriteAgentNameForListDisplay( key: string, @@ -11,7 +11,7 @@ function rewriteAgentNameForListDisplay( const agent = value as Record return { ...agent, - name: getAgentRuntimeName(key), + name: getAgentListDisplayName(key), } } diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index 0c4ea1cea..d2b0b9ea9 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -3,7 +3,7 @@ import { describe, test, expect, spyOn, beforeEach, afterEach, mock } from "bun:test" import type { CategoryConfig } from "../config/schema" import type { OhMyOpenCodeConfig } from "../config" -import { getAgentDisplayName, getAgentListDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" +import { getAgentDisplayName, getAgentListDisplayName } from "../shared/agent-display-names" import { resolveCategoryConfig } from "./category-config-resolver" import * as agents from "../agents" @@ -359,19 +359,19 @@ describe("Plan agent demote behavior", () => { expect(emittedCoreEntries).toEqual([ [ getAgentListDisplayName("sisyphus"), - expect.objectContaining({ name: getAgentRuntimeName("sisyphus") }), + expect.objectContaining({ name: getAgentListDisplayName("sisyphus") }), ], [ getAgentListDisplayName("hephaestus"), - expect.objectContaining({ name: getAgentRuntimeName("hephaestus") }), + expect.objectContaining({ name: getAgentListDisplayName("hephaestus") }), ], [ getAgentListDisplayName("prometheus"), - expect.objectContaining({ name: getAgentRuntimeName("prometheus") }), + expect.objectContaining({ name: getAgentListDisplayName("prometheus") }), ], [ getAgentListDisplayName("atlas"), - expect.objectContaining({ name: getAgentRuntimeName("atlas") }), + expect.objectContaining({ name: getAgentListDisplayName("atlas") }), ], ]) }) @@ -540,7 +540,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("canonicalizes configured default_agent when key uses mixed case", async () => { @@ -564,7 +564,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("canonicalizes configured default_agent key to display name", async () => { @@ -588,7 +588,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // #then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("preserves existing display-name default_agent", async () => { @@ -613,7 +613,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // #then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("sets default_agent to sisyphus when missing", async () => { @@ -636,7 +636,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // #then - expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) + expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) }) test("uses canonical default_agent display name so OpenCode lookups match emitted agent keys", async () => { @@ -660,7 +660,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("sets default_agent to sisyphus when configured default_agent is empty after trim", async () => { @@ -684,7 +684,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) + expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) }) test("preserves custom default_agent names while trimming whitespace", async () => { diff --git a/src/plugin-interface.test.ts b/src/plugin-interface.test.ts index c877fdc95..211bfe90c 100644 --- a/src/plugin-interface.test.ts +++ b/src/plugin-interface.test.ts @@ -6,7 +6,6 @@ import { randomUUID } from "node:crypto" import { createPluginInterface } from "./plugin-interface" import { createAutoSlashCommandHook } from "./hooks/auto-slash-command" import { createStartWorkHook } from "./hooks/start-work" -import { getAgentListDisplayName } from "./shared/agent-display-names" import { readBoulderState } from "./features/boulder-state" import { _resetForTesting, @@ -258,3 +257,50 @@ describe("createPluginInterface - ulw-loop native command smoke", () => { ]) }) }) + +describe("createPluginInterface - backward compatibility", () => { + beforeEach(() => { + _resetForTesting() + registerAgentName("hephaestus") + }) + + afterEach(() => { + _resetForTesting() + }) + + test("strips legacy ZWSP-prefixed agent names from persisted chat.message session state (GH-3259)", async () => { + // given - persisted session payload from v3.14.0-v3.16.0 with ZWSP prefix + const pluginInterface = createPluginInterface({ + ctx: { + directory: tmpdir(), + client: { tui: { showToast: async () => {} } }, + } as never, + pluginConfig: {} as never, + firstMessageVariantGate: { + shouldOverride: () => false, + markApplied: () => {}, + markSessionCreated: () => {}, + clear: () => {}, + }, + managers: {} as never, + hooks: {} as never, + tools: {}, + }) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "hello" }], + } + + // when + await pluginInterface["chat.message"]?.( + { + sessionID: "ses-legacy-zwsp", + agent: "\u200B\u200BHephaestus - Deep Agent", + } as never, + output as never, + ) + + // then + expect(getSessionAgent("ses-legacy-zwsp")).toBe("Hephaestus - Deep Agent") + }) +}) diff --git a/src/plugin/chat-message.test.ts b/src/plugin/chat-message.test.ts index e2e813cd8..ab6725bec 100644 --- a/src/plugin/chat-message.test.ts +++ b/src/plugin/chat-message.test.ts @@ -767,4 +767,18 @@ describe("createChatMessageHandler - TUI variant passthrough", () => { expect(output.message["model"]).toBeUndefined() expect(getSessionModel("test-session")).toEqual(nextModel) }) + + test("strips legacy ZWSP-prefixed agent names from persisted prompt body session state (GH-3259)", async () => { + //#given - persisted prompt body from v3.14.0-v3.16.0 may contain ZWSP-prefixed agent + const args = createMockHandlerArgs() + const handler = createChatMessageHandler(args) + const input = createMockInput("\u200B\u200BHephaestus - Deep Agent") + const output = createMockOutput() + + //#when + await handler(input, output) + + //#then + expect(getSessionAgent("test-session")).toBe("Hephaestus - Deep Agent") + }) }) diff --git a/src/shared/agent-display-names.test.ts b/src/shared/agent-display-names.test.ts index 2c3d732cd..92798275d 100644 --- a/src/shared/agent-display-names.test.ts +++ b/src/shared/agent-display-names.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "bun:test" -import { AGENT_DISPLAY_NAMES, getAgentConfigKey, getAgentDisplayName, getAgentListDisplayName, normalizeAgentForPrompt, normalizeAgentForPromptKey } from "./agent-display-names" +import { AGENT_DISPLAY_NAMES, getAgentConfigKey, getAgentDisplayName, getAgentListDisplayName, normalizeAgentForPrompt, normalizeAgentForPromptKey, stripAgentListSortPrefix } from "./agent-display-names" describe("getAgentDisplayName", () => { it("returns display name for lowercase config key (new format)", () => { @@ -194,16 +194,26 @@ describe("getAgentConfigKey", () => { }) describe("getAgentListDisplayName", () => { - it("applies invisible stable-sort prefixes to the core agent list", () => { - expect(getAgentListDisplayName("sisyphus")).toBe("\u200BSisyphus - Ultraworker") - expect(getAgentListDisplayName("hephaestus")).toBe("\u200B\u200BHephaestus - Deep Agent") - expect(getAgentListDisplayName("prometheus")).toBe("\u200B\u200B\u200BPrometheus - Plan Builder") - expect(getAgentListDisplayName("atlas")).toBe("\u200B\u200B\u200B\u200BAtlas - Plan Executor") + it("returns the canonical display name for the core agent list", () => { + expect(getAgentListDisplayName("sisyphus")).toBe("Sisyphus - Ultraworker") + expect(getAgentListDisplayName("hephaestus")).toBe("Hephaestus - Deep Agent") + expect(getAgentListDisplayName("prometheus")).toBe("Prometheus - Plan Builder") + expect(getAgentListDisplayName("atlas")).toBe("Atlas - Plan Executor") }) - it("keeps non-core agents unprefixed for list display", () => { + it("keeps non-core agents unchanged for list display", () => { expect(getAgentListDisplayName("oracle")).toBe("oracle") }) + + it("is a thin alias for getAgentDisplayName", () => { + expect(getAgentListDisplayName("sisyphus")).toBe(getAgentDisplayName("sisyphus")) + }) +}) + +describe("stripAgentListSortPrefix", () => { + it("strips legacy zero-width sort prefixes baked into v3.14.0–v3.16.0 sessions", () => { + expect(stripAgentListSortPrefix("\u200B\u200BHephaestus - Deep Agent")).toBe("Hephaestus - Deep Agent") + }) }) describe("normalizeAgentForPrompt", () => { diff --git a/src/shared/agent-display-names.ts b/src/shared/agent-display-names.ts index 324fac785..55dc1918d 100644 --- a/src/shared/agent-display-names.ts +++ b/src/shared/agent-display-names.ts @@ -26,13 +26,6 @@ export const AGENT_DISPLAY_NAMES: Record = { "council-member": "council-member", } -const AGENT_LIST_SORT_PREFIXES: Record = { - sisyphus: "\u200B", - hephaestus: "\u200B\u200B", - prometheus: "\u200B\u200B\u200B", - atlas: "\u200B\u200B\u200B\u200B", -} - const INVISIBLE_AGENT_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g export function stripInvisibleAgentCharacters(agentName: string): string { @@ -43,13 +36,6 @@ export function stripAgentListSortPrefix(agentName: string): string { return stripInvisibleAgentCharacters(agentName) } -export function getAgentRuntimeName(configKey: string): string { - const displayName = getAgentDisplayName(configKey) - const prefix = AGENT_LIST_SORT_PREFIXES[configKey.toLowerCase()] - - return prefix ? `${prefix}${displayName}` : displayName -} - /** * Get display name for an agent config key. * Uses case-insensitive lookup for backward compatibility. @@ -59,22 +45,28 @@ export function getAgentDisplayName(configKey: string): string { // Try exact match first const exactMatch = AGENT_DISPLAY_NAMES[configKey] if (exactMatch !== undefined) return exactMatch - + // Fall back to case-insensitive search const lowerKey = configKey.toLowerCase() for (const [k, v] of Object.entries(AGENT_DISPLAY_NAMES)) { if (k.toLowerCase() === lowerKey) return v } - + // Unknown agent: return original key return configKey } /** - * Runtime-facing agent name used for OpenCode list ordering. + * Thin alias for `getAgentDisplayName` preserved for external imports. + * + * Earlier versions injected zero-width prefixes here to bias OpenCode's + * `agent.name` sort. Sort ordering is now enforced by + * `src/shared/agent-sort-shim.ts`, so this function emits the canonical + * display name verbatim. Kept exported because downstream modules still + * import this symbol; do not collapse the call sites without coordinating. */ export function getAgentListDisplayName(configKey: string): string { - return getAgentRuntimeName(configKey) + return getAgentDisplayName(configKey) } const REVERSE_DISPLAY_NAMES: Record = Object.fromEntries( 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 fe6ffff96..5346f9eb8 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 @@ -963,4 +963,24 @@ describe("resolveSubagentExecution - agent name sanitization", () => { expect(result.error).toBeUndefined() expect(result.agentToUse).toBe("Sisyphus - Ultraworker") }) + + test("strips legacy ZWSP-prefixed agent names from persisted subagent runtime state (GH-3259)", async () => { + //#given - persisted runtime agent metadata from v3.14.0-v3.16.0 with ZWSP prefix + readProviderModelsCacheMock.mockReturnValue({ + models: {}, + connected: [], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const args = createBaseArgs({ subagent_type: "Hephaestus - Deep Agent" }) + const executorCtx = createExecutorContext(async () => ([ + { name: "\u200B\u200BHephaestus - Deep Agent", 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("Hephaestus - Deep Agent") + }) })