From 1e42c610262e5aa0d12edd3a9b9781160ac0c6cf Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 23:07:52 +0900 Subject: [PATCH] fix(todo-continuation): normalize prompt agent Todo continuation could reinject with a lowercase built-in config key such as hephaestus when no registered alias was available. OpenCode prompt dispatch expects the prompt-facing agent name, so the continuation failed with an agent-not-found error. Normalize the dispatch fallback through the existing prompt agent display-name resolver and pin the lowercase Hephaestus regression. --- .../continuation-injection-agent-name.test.ts | 48 +++++++++++++++++++ .../continuation-injection.ts | 4 +- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/hooks/todo-continuation-enforcer/continuation-injection-agent-name.test.ts diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection-agent-name.test.ts b/src/hooks/todo-continuation-enforcer/continuation-injection-agent-name.test.ts new file mode 100644 index 000000000..093ad886e --- /dev/null +++ b/src/hooks/todo-continuation-enforcer/continuation-injection-agent-name.test.ts @@ -0,0 +1,48 @@ +/// + +import { afterEach, describe, expect, test } from "bun:test" + +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" + +import { releaseAllPromptAsyncReservationsForTesting } from "../shared/prompt-async-gate" +import { injectContinuation } from "./continuation-injection" + +describe("injectContinuation agent names", () => { + afterEach(() => { + releaseAllPromptAsyncReservationsForTesting() + }) + + test("#given resolved agent is a lowercase built-in config key #when continuation is injected #then promptAsync receives the registered display name", async () => { + // given + let capturedAgent: string | undefined + const ctx = unsafeTestValue[0]["ctx"]>({ + directory: "/tmp/test", + client: { + session: { + todo: async () => ({ data: [{ id: "1", content: "todo", status: "pending", priority: "high" }] }), + promptAsync: async (input: { body: { agent?: string } }) => { + capturedAgent = input.body.agent + return {} + }, + }, + }, + }) + const sessionStateStore = unsafeTestValue[0]["sessionStateStore"]>({ + getExistingState: () => ({ inFlight: false, lastInjectedAt: 0, consecutiveFailures: 0 }), + }) + + // when + await injectContinuation({ + ctx, + sessionID: "ses_lowercase_builtin_agent", + resolvedInfo: { + agent: "hephaestus", + model: { providerID: "openai", modelID: "gpt-5.5" }, + }, + sessionStateStore, + }) + + // then + expect(capturedAgent).toBe("Hephaestus - Deep Agent") + }) +}) diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.ts index 5609680c7..f22da95f7 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.ts @@ -20,8 +20,8 @@ import { log } from "../../shared/logger" import { isSqliteBackend } from "../../shared/opencode-storage-detection" import { getAgentConfigKey, + normalizeAgentForPrompt, normalizeAgentForPromptKey, - stripAgentListSortPrefix, } from "../../shared/agent-display-names" import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../shared/prompt-async-gate" @@ -134,7 +134,7 @@ export async function injectContinuation(args: { const promptAgent = normalizeAgentForPromptKey(agentName) const resolvedAgent = resolveRegisteredAgentName(agentName) - const launchAgent = resolvedAgent ? stripAgentListSortPrefix(resolvedAgent) : resolvedAgent + const launchAgent = normalizeAgentForPrompt(resolvedAgent ?? agentName) if (promptAgent && skipAgents.some(s => getAgentConfigKey(s) === getAgentConfigKey(promptAgent))) { log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName })