fix(agents): strip ZWSP ordering prefixes in session state, config lookups, and override protection
Prevent ZWSP sort prefixes from leaking into stored agent names, config key lookups, and override-protection normalization. Ensures prefixed list-display names resolve correctly throughout the pipeline. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) assistance
This commit is contained in:
@@ -37,6 +37,18 @@ describe("claude-code-session-state", () => {
|
||||
expect(getSessionAgent(sessionID)).toBe(agent)
|
||||
})
|
||||
|
||||
test("should strip zero-width ordering prefixes before storing agent for session", () => {
|
||||
// given
|
||||
const sessionID = "test-session-prefixed"
|
||||
const agent = "\u200B\u200B\u200BPrometheus (Plan Builder)"
|
||||
|
||||
// when
|
||||
setSessionAgent(sessionID, agent)
|
||||
|
||||
// then
|
||||
expect(getSessionAgent(sessionID)).toBe("Prometheus (Plan Builder)")
|
||||
})
|
||||
|
||||
test("should NOT overwrite existing agent (first-write wins)", () => {
|
||||
// given
|
||||
const sessionID = "test-session-1"
|
||||
@@ -69,6 +81,18 @@ describe("claude-code-session-state", () => {
|
||||
// then
|
||||
expect(getSessionAgent(sessionID)).toBe("sisyphus")
|
||||
})
|
||||
|
||||
test("should strip zero-width ordering prefixes when overwriting existing agent", () => {
|
||||
// given
|
||||
const sessionID = "test-session-prefixed-update"
|
||||
setSessionAgent(sessionID, "sisyphus")
|
||||
|
||||
// when
|
||||
updateSessionAgent(sessionID, "\u200B\u200BHephaestus (Deep Agent)")
|
||||
|
||||
// then
|
||||
expect(getSessionAgent(sessionID)).toBe("Hephaestus (Deep Agent)")
|
||||
})
|
||||
})
|
||||
|
||||
describe("clearSessionAgent", () => {
|
||||
|
||||
@@ -21,6 +21,10 @@ function normalizeRegisteredAgentName(name: string): string {
|
||||
return name.replace(ZERO_WIDTH_CHARACTERS_REGEX, "").toLowerCase()
|
||||
}
|
||||
|
||||
function normalizeStoredAgentName(name: string): string {
|
||||
return name.replace(ZERO_WIDTH_CHARACTERS_REGEX, "")
|
||||
}
|
||||
|
||||
export function registerAgentName(name: string): void {
|
||||
const normalizedName = normalizeRegisteredAgentName(name)
|
||||
registeredAgentNames.add(normalizedName)
|
||||
@@ -48,12 +52,12 @@ const sessionAgentMap = new Map<string, string>()
|
||||
|
||||
export function setSessionAgent(sessionID: string, agent: string): void {
|
||||
if (!sessionAgentMap.has(sessionID)) {
|
||||
sessionAgentMap.set(sessionID, agent)
|
||||
sessionAgentMap.set(sessionID, normalizeStoredAgentName(agent))
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSessionAgent(sessionID: string, agent: string): void {
|
||||
sessionAgentMap.set(sessionID, agent)
|
||||
sessionAgentMap.set(sessionID, normalizeStoredAgentName(agent))
|
||||
}
|
||||
|
||||
export function getSessionAgent(sessionID: string): string | undefined {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const PARENTHETICAL_SUFFIX_PATTERN = /\s*(\([^)]*\)\s*)+$/u
|
||||
const ZERO_WIDTH_CHARACTERS_PATTERN = /[\u200B\u200C\u200D\uFEFF]/g
|
||||
|
||||
export function normalizeProtectedAgentName(agentName: string): string {
|
||||
return agentName
|
||||
.replace(ZERO_WIDTH_CHARACTERS_PATTERN, "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(PARENTHETICAL_SUFFIX_PATTERN, "")
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createAutoSlashCommandHook } from "../hooks/auto-slash-command"
|
||||
import { createStartWorkHook } from "../hooks/start-work"
|
||||
import { readBoulderState } from "../features/boulder-state"
|
||||
import { _resetForTesting, setMainSession, subagentSessions, registerAgentName, updateSessionAgent, getSessionAgent } from "../features/claude-code-session-state"
|
||||
import { getAgentListDisplayName } from "../shared/agent-display-names"
|
||||
import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state"
|
||||
|
||||
type ChatMessagePart = { type: string; text?: string; [key: string]: unknown }
|
||||
@@ -374,6 +375,31 @@ describe("createChatMessageHandler - TUI variant passthrough", () => {
|
||||
expect(getSessionModel("test-session")).toEqual({ providerID: "openai", modelID: "gpt-5.4" })
|
||||
})
|
||||
|
||||
test("treats prefixed list-display agent names as explicit model overrides", async () => {
|
||||
//#given
|
||||
setMainSession("test-session")
|
||||
setSessionModel("test-session", { providerID: "openai", modelID: "gpt-5.4" })
|
||||
const args = createMockHandlerArgs({
|
||||
shouldOverride: false,
|
||||
pluginConfig: {
|
||||
agents: {
|
||||
prometheus: { model: "anthropic/claude-opus-4-6" },
|
||||
},
|
||||
},
|
||||
})
|
||||
const handler = createChatMessageHandler(args)
|
||||
const input = createMockInput(getAgentListDisplayName("prometheus"))
|
||||
const output = createMockOutput()
|
||||
|
||||
//#when
|
||||
await handler(input, output)
|
||||
|
||||
//#then
|
||||
expect(output.message["model"]).toBeUndefined()
|
||||
expect(getSessionModel("test-session")).toEqual({ providerID: "openai", modelID: "gpt-5.4" })
|
||||
expect(getSessionAgent("test-session")).toBe("Prometheus (Plan Builder)")
|
||||
})
|
||||
|
||||
test("respects a mid-conversation model switch instead of reusing the previous stored model", async () => {
|
||||
//#given
|
||||
setMainSession("test-session")
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { OhMyOpenCodeConfig } from "../config"
|
||||
import type { PluginContext } from "./types"
|
||||
|
||||
import { hasConnectedProvidersCache } from "../shared"
|
||||
import { getAgentConfigKey } from "../shared/agent-display-names"
|
||||
import { getSessionModel, setSessionModel } from "../shared/session-model-state"
|
||||
import { getMainSessionID, setSessionAgent, subagentSessions } from "../features/claude-code-session-state"
|
||||
import { applyUltraworkModelOverrideOnMessage } from "./ultrawork-model-override"
|
||||
@@ -46,11 +47,12 @@ function hasExplicitAgentModelOverride(
|
||||
pluginConfig: OhMyOpenCodeConfig
|
||||
): boolean {
|
||||
const configuredAgents = pluginConfig.agents
|
||||
if (!agent || !configuredAgents || !(agent in configuredAgents)) {
|
||||
const normalizedAgent = typeof agent === "string" ? getAgentConfigKey(agent) : undefined
|
||||
if (!normalizedAgent || !configuredAgents || !(normalizedAgent in configuredAgents)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const configuredAgent = configuredAgents[agent as keyof typeof configuredAgents]
|
||||
const configuredAgent = configuredAgents[normalizedAgent as keyof typeof configuredAgents]
|
||||
const configuredModel = configuredAgent?.model
|
||||
return typeof configuredModel === "string" && configuredModel.trim().length > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user