From 560d13dc70e4b7ed715130d4746b77a311ff790e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 16 Feb 2026 20:43:09 +0900 Subject: [PATCH] Normalize agent name comparisons to handle display name keys Hooks and tools now use getAgentConfigKey() to resolve agent names (which may be display names like 'Atlas (Plan Executor)') to lowercase config keys before comparison. - session-utils: orchestrator check uses getAgentConfigKey - atlas event-handler: boulder agent matching uses config keys - category-skill-reminder: target agent check uses config keys - todo-continuation-enforcer: skipAgents comparison normalized - subagent-resolver: resolves 'metis' -> 'Metis (Plan Consultant)' for lookup --- src/hooks/atlas/event-handler.ts | 8 +++++--- src/hooks/category-skill-reminder/hook.ts | 9 +++++---- .../continuation-injection.ts | 3 ++- src/hooks/todo-continuation-enforcer/idle-event.ts | 6 ++++-- src/shared/session-utils.ts | 5 +++-- src/tools/delegate-task/subagent-resolver.ts | 14 +++++++++----- 6 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/hooks/atlas/event-handler.ts b/src/hooks/atlas/event-handler.ts index 68c89e485..76a3a5004 100644 --- a/src/hooks/atlas/event-handler.ts +++ b/src/hooks/atlas/event-handler.ts @@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import { getPlanProgress, readBoulderState } from "../../features/boulder-state" import { subagentSessions } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" +import { getAgentConfigKey } from "../../shared/agent-display-names" import { HOOK_NAME } from "./hook-name" import { isAbortError } from "./is-abort-error" import { injectBoulderContinuation } from "./boulder-continuation-injector" @@ -88,11 +89,12 @@ export function createAtlasEventHandler(input: { } const lastAgent = await getLastAgentFromSession(sessionID, ctx.client) - const requiredAgent = (boulderState.agent ?? "atlas").toLowerCase() - const lastAgentMatchesRequired = lastAgent === requiredAgent + const lastAgentKey = getAgentConfigKey(lastAgent ?? "") + const requiredAgent = getAgentConfigKey(boulderState.agent ?? "atlas") + const lastAgentMatchesRequired = lastAgentKey === requiredAgent const boulderAgentWasNotExplicitlySet = boulderState.agent === undefined const boulderAgentDefaultsToAtlas = requiredAgent === "atlas" - const lastAgentIsSisyphus = lastAgent === "sisyphus" + const lastAgentIsSisyphus = lastAgentKey === "sisyphus" const allowSisyphusWhenDefaultAtlas = boulderAgentWasNotExplicitlySet && boulderAgentDefaultsToAtlas && lastAgentIsSisyphus const agentMatches = lastAgentMatchesRequired || allowSisyphusWhenDefaultAtlas if (!agentMatches) { diff --git a/src/hooks/category-skill-reminder/hook.ts b/src/hooks/category-skill-reminder/hook.ts index b15715cda..a89d182b0 100644 --- a/src/hooks/category-skill-reminder/hook.ts +++ b/src/hooks/category-skill-reminder/hook.ts @@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder" import { getSessionAgent } from "../../features/claude-code-session-state" import { log } from "../../shared" +import { getAgentConfigKey } from "../../shared/agent-display-names" import { buildReminderMessage } from "./formatter" /** @@ -75,11 +76,11 @@ export function createCategorySkillReminderHook( function isTargetAgent(sessionID: string, inputAgent?: string): boolean { const agent = getSessionAgent(sessionID) ?? inputAgent if (!agent) return false - const agentLower = agent.toLowerCase() + const agentKey = getAgentConfigKey(agent) return ( - TARGET_AGENTS.has(agentLower) || - agentLower.includes("sisyphus") || - agentLower.includes("atlas") + TARGET_AGENTS.has(agentKey) || + agentKey.includes("sisyphus") || + agentKey.includes("atlas") ) } diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.ts index e9c36b470..a8e8586e6 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.ts @@ -9,6 +9,7 @@ import { } from "../../features/hook-message-injector" import { log } from "../../shared/logger" import { isSqliteBackend } from "../../shared/opencode-storage-detection" +import { getAgentConfigKey } from "../../shared/agent-display-names" import { CONTINUATION_PROMPT, @@ -103,7 +104,7 @@ export async function injectContinuation(args: { tools = tools ?? previousMessage?.tools } - if (agentName && skipAgents.includes(agentName)) { + if (agentName && skipAgents.some(s => getAgentConfigKey(s) === getAgentConfigKey(agentName))) { log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName }) return } diff --git a/src/hooks/todo-continuation-enforcer/idle-event.ts b/src/hooks/todo-continuation-enforcer/idle-event.ts index d97a9b6bf..689672c01 100644 --- a/src/hooks/todo-continuation-enforcer/idle-event.ts +++ b/src/hooks/todo-continuation-enforcer/idle-event.ts @@ -4,6 +4,7 @@ import type { BackgroundManager } from "../../features/background-agent" import type { ToolPermission } from "../../features/hook-message-injector" import { normalizeSDKResponse } from "../../shared" import { log } from "../../shared/logger" +import { getAgentConfigKey } from "../../shared/agent-display-names" import { ABORT_WINDOW_MS, @@ -162,8 +163,9 @@ export async function handleSessionIdle(args: { log(`[${HOOK_NAME}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage }) - if (resolvedInfo?.agent && skipAgents.includes(resolvedInfo.agent)) { - log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedInfo.agent }) + const resolvedAgentName = resolvedInfo?.agent + if (resolvedAgentName && skipAgents.some(s => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) { + log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName }) return } if (hasCompactionMessage && !resolvedInfo?.agent) { diff --git a/src/shared/session-utils.ts b/src/shared/session-utils.ts index 5a9d33065..5884da784 100644 --- a/src/shared/session-utils.ts +++ b/src/shared/session-utils.ts @@ -2,6 +2,7 @@ import { findNearestMessageWithFields, findNearestMessageWithFieldsFromSDK } fro import { getMessageDir } from "./opencode-message-dir" import { isSqliteBackend } from "./opencode-storage-detection" import { log } from "./logger" +import { getAgentConfigKey } from "./agent-display-names" import type { PluginInput } from "@opencode-ai/plugin" export async function isCallerOrchestrator(sessionID?: string, client?: PluginInput["client"]): Promise { @@ -10,7 +11,7 @@ export async function isCallerOrchestrator(sessionID?: string, client?: PluginIn if (isSqliteBackend() && client) { try { const nearest = await findNearestMessageWithFieldsFromSDK(client, sessionID) - return nearest?.agent?.toLowerCase() === "atlas" + return getAgentConfigKey(nearest?.agent ?? "") === "atlas" } catch (error) { log("[session-utils] SDK orchestrator check failed", { sessionID, error: String(error) }) return false @@ -20,5 +21,5 @@ export async function isCallerOrchestrator(sessionID?: string, client?: PluginIn const messageDir = getMessageDir(sessionID) if (!messageDir) return false const nearest = findNearestMessageWithFields(messageDir) - return nearest?.agent?.toLowerCase() === "atlas" + return getAgentConfigKey(nearest?.agent ?? "") === "atlas" } diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index 79226a002..5651fba29 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -4,6 +4,7 @@ import { isPlanFamily } from "./constants" import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent" import { parseModelString } from "./model-string-parser" import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements" +import { getAgentDisplayName, getAgentConfigKey } from "../../shared/agent-display-names" import { normalizeSDKResponse } from "../../shared" import { getAvailableModelsForDelegateTask } from "./available-models" import { resolveModelForDelegateTask } from "./model-selection" @@ -54,13 +55,16 @@ Create the work plan directly - that's your job as the planning agent.`, const callableAgents = agents.filter((a) => a.mode !== "primary") + const resolvedDisplayName = getAgentDisplayName(agentToUse) const matchedAgent = callableAgents.find( (agent) => agent.name.toLowerCase() === agentToUse.toLowerCase() + || agent.name.toLowerCase() === resolvedDisplayName.toLowerCase() ) if (!matchedAgent) { const isPrimaryAgent = agents .filter((a) => a.mode === "primary") - .find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()) + .find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase() + || agent.name.toLowerCase() === resolvedDisplayName.toLowerCase()) if (isPrimaryAgent) { return { @@ -83,10 +87,10 @@ Create the work plan directly - that's your job as the planning agent.`, agentToUse = matchedAgent.name - const agentNameLower = agentToUse.toLowerCase() - const agentOverride = agentOverrides?.[agentNameLower as keyof typeof agentOverrides] - ?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentNameLower)?.[1] : undefined) - const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentNameLower] + const agentConfigKey = getAgentConfigKey(agentToUse) + const agentOverride = agentOverrides?.[agentConfigKey as keyof typeof agentOverrides] + ?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentConfigKey)?.[1] : undefined) + const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentConfigKey] if (agentOverride?.model || agentRequirement || matchedAgent.model) { const availableModels = await getAvailableModelsForDelegateTask(client)