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") + }) })