fix(continuation): resolve registered agent name before dispatching prompt (#4417)
/start-work was unresponsive under Atlas because ralph-loop and todo-continuation-enforcer normalized the inherited agent to a config key (e.g. 'atlas') while OpenCode only accepts the registered display name (e.g. 'Atlas (Plan Executor)'), producing 'Agent not found' on dispatch. Prefer resolveRegisteredAgentName(agent) and fall back to normalizeAgentForPromptKey only when no registration exists, mirroring the start-work hook's resolution chain. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
import {
|
||||
_resetForTesting,
|
||||
registerAgentName,
|
||||
} from "../../features/claude-code-session-state"
|
||||
import { releaseAllPromptAsyncReservationsForTesting } from "../shared/prompt-async-gate"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
import { injectContinuationPrompt } from "./continuation-prompt-injector"
|
||||
|
||||
describe("ralph-loop continuation prompt agent resolution", () => {
|
||||
afterEach(() => {
|
||||
releaseAllPromptAsyncReservationsForTesting()
|
||||
_resetForTesting()
|
||||
})
|
||||
|
||||
test("#given OpenCode registered Atlas under legacy display name #when inherited agent is config key #then prompt uses registered name", async () => {
|
||||
// given
|
||||
registerAgentName("Atlas (Plan Executor)")
|
||||
let capturedAgent: string | undefined
|
||||
const ctx = unsafeTestValue<PluginInput>({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({ data: [{ info: { agent: "atlas" } }] }),
|
||||
promptAsync: async (input: { readonly body: { readonly agent?: string } }) => {
|
||||
capturedAgent = input.body.agent
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// when
|
||||
await injectContinuationPrompt(ctx, {
|
||||
sessionID: "ses_ralph_registered_atlas",
|
||||
prompt: "continue",
|
||||
directory: "/tmp/test",
|
||||
apiTimeoutMs: 50,
|
||||
})
|
||||
|
||||
// then
|
||||
expect(capturedAgent).toBe("Atlas (Plan Executor)")
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
normalizeSDKResponse,
|
||||
resolveInheritedPromptTools,
|
||||
} from "../../shared"
|
||||
import { normalizeAgentForPrompt, stripAgentListSortPrefix } from "../../shared/agent-display-names"
|
||||
import { resolveRegisteredAgentName } from "../../features/claude-code-session-state"
|
||||
import { normalizeAgentForPromptKey, stripAgentListSortPrefix } from "../../shared/agent-display-names"
|
||||
import { dispatchInternalPrompt } from "../shared/prompt-async-gate"
|
||||
|
||||
type MessageInfo = {
|
||||
@@ -62,20 +63,10 @@ function createPromptAsyncError(prefix: string, error: unknown): Error {
|
||||
}
|
||||
|
||||
function normalizeInheritedAgentForPrompt(agent: string | undefined): string | undefined {
|
||||
if (typeof agent !== "string") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const inheritedAgent = stripAgentListSortPrefix(agent).trim()
|
||||
if (!inheritedAgent) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (inheritedAgent.includes(" - ")) {
|
||||
return inheritedAgent
|
||||
}
|
||||
|
||||
return normalizeAgentForPrompt(inheritedAgent)
|
||||
const resolvedAgent = resolveRegisteredAgentName(agent) ?? normalizeAgentForPromptKey(agent)
|
||||
if (typeof resolvedAgent !== "string") return undefined
|
||||
const cleanAgent = stripAgentListSortPrefix(resolvedAgent).trim()
|
||||
return cleanAgent || undefined
|
||||
}
|
||||
|
||||
export async function injectContinuationPrompt(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
import {
|
||||
_resetForTesting,
|
||||
registerAgentName,
|
||||
} from "../../features/claude-code-session-state"
|
||||
import { releaseAllPromptAsyncReservationsForTesting } from "../shared/prompt-async-gate"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
import { injectContinuation } from "./continuation-injection"
|
||||
|
||||
describe("todo continuation registered agent resolution", () => {
|
||||
afterEach(() => {
|
||||
releaseAllPromptAsyncReservationsForTesting()
|
||||
_resetForTesting()
|
||||
})
|
||||
|
||||
test("#given OpenCode registered Atlas under legacy display name #when continuation inherits config key #then prompt uses registered name", async () => {
|
||||
// given
|
||||
registerAgentName("Atlas (Plan Executor)")
|
||||
let capturedAgent: string | undefined
|
||||
const ctx = unsafeTestValue<PluginInput>({
|
||||
directory: "/tmp/test",
|
||||
client: {
|
||||
session: {
|
||||
todo: async () => ({ data: [{ id: "1", content: "todo", status: "pending", priority: "high" }] }),
|
||||
promptAsync: async (input: { readonly body: { readonly agent?: string } }) => {
|
||||
capturedAgent = input.body.agent
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const sessionStateStore = {
|
||||
getExistingState: () => ({ inFlight: false, lastInjectedAt: 0, consecutiveFailures: 0 }),
|
||||
}
|
||||
|
||||
// when
|
||||
await injectContinuation({
|
||||
ctx,
|
||||
sessionID: "ses_todo_registered_atlas",
|
||||
resolvedInfo: {
|
||||
agent: "atlas",
|
||||
model: { providerID: "openai", modelID: "gpt-5.5" },
|
||||
},
|
||||
sessionStateStore: unsafeTestValue(sessionStateStore),
|
||||
})
|
||||
|
||||
// then
|
||||
expect(capturedAgent).toBe("Atlas (Plan Executor)")
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
@@ -132,9 +132,8 @@ export async function injectContinuation(args: {
|
||||
tools = tools ?? previousMessage?.tools
|
||||
}
|
||||
|
||||
const promptAgent = normalizeAgentForPromptKey(agentName)
|
||||
const resolvedAgent = resolveRegisteredAgentName(agentName)
|
||||
const launchAgent = normalizeAgentForPrompt(resolvedAgent ?? agentName)
|
||||
const promptAgent = resolveRegisteredAgentName(agentName) ?? normalizeAgentForPromptKey(agentName)
|
||||
const launchAgent = promptAgent ? stripAgentListSortPrefix(promptAgent).trim() || undefined : undefined
|
||||
|
||||
if (promptAgent && skipAgents.some(s => getAgentConfigKey(s) === getAgentConfigKey(promptAgent))) {
|
||||
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName })
|
||||
|
||||
Reference in New Issue
Block a user