Merge pull request #4046 from code-yeongyu/fix/3494-strip-zwsp-before-promptasync

fix(atlas,todo-continuation): strip ZWSP sort prefix before promptAsync agent (fixes #3494, supersedes #3547)
This commit is contained in:
YeonGyu-Kim
2026-05-15 19:45:21 +09:00
committed by GitHub
4 changed files with 78 additions and 2 deletions
@@ -3,6 +3,7 @@ import {
isAgentRegistered,
resolveRegisteredAgentName,
} from "../../features/claude-code-session-state"
import { stripAgentListSortPrefix } from "../../shared/agent-display-names"
import { log } from "../../shared/logger"
import { createInternalAgentContinuationTextPart, resolveInheritedPromptTools } from "../../shared"
import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate"
@@ -67,9 +68,10 @@ export async function injectBoulderContinuation(input: {
`\n\n[Status: ${total - remaining}/${total} completed, ${remaining} remaining]` +
preferredSessionContext +
worktreeContext
const continuationAgent = resolveRegisteredAgentName(
const resolvedContinuationAgent = resolveRegisteredAgentName(
agent ?? (isAgentRegistered("atlas") ? "atlas" : undefined),
)
const continuationAgent = resolvedContinuationAgent ? stripAgentListSortPrefix(resolvedContinuationAgent) : resolvedContinuationAgent
if (!continuationAgent || !isAgentRegistered(continuationAgent)) {
log(`[${HOOK_NAME}] Skipped injection: continuation agent unavailable`, {
+33
View File
@@ -2070,6 +2070,39 @@ session_id: ses_untrusted_999
expect(callArgs.body.agent).not.toBe("atlas")
})
test("#given boulder agent registered with ZWSP sort prefix #when continuation injects #then promptAsync receives display name without ZWSP", async () => {
// given - OpenCode TUI registers agent names with leading ZWSP for sort ordering
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
agent: "\u200B\u200BAtlas - Plan Executor",
}
writeBoulderState(TEST_DIR, state)
registerAgentName("\u200B\u200BAtlas - Plan Executor")
const mockInput = createMockPluginInput()
const hook = createTestAtlasHook(mockInput)
// when
await hook.handler({
event: {
type: "session.idle",
properties: { sessionID: MAIN_SESSION_ID },
},
})
// then
expect(mockInput._promptMock).toHaveBeenCalled()
const callArgs = mockInput._promptMock.mock.calls[0][0]
expect(callArgs.body.agent).toBe("Atlas - Plan Executor")
expect(callArgs.body.agent).not.toContain("\u200B")
})
test("should debounce rapid continuation injections (prevent infinite loop)", async () => {
// given - boulder state with incomplete plan
const planPath = join(TEST_DIR, "test-plan.md")
@@ -43,6 +43,45 @@ describe("injectContinuation", () => {
expect(capturedAgent).toBe("Sisyphus - Ultraworker")
})
test("#given resolved agent name still carries a ZWSP sort prefix #when continuation is injected #then promptAsync receives the agent name without the ZWSP prefix", async () => {
// given
let capturedAgent: string | undefined
const 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 = {
getExistingState: () => ({ inFlight: false, lastInjectedAt: 0, consecutiveFailures: 0 }),
}
// when
await injectContinuation({
ctx: ctx as never,
sessionID: "ses_zwsp_agent",
resolvedInfo: {
agent: "\u200B\u200BSisyphus - Ultraworker",
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
},
sessionStateStore: sessionStateStore as never,
})
// then
expect(capturedAgent).toBe("Sisyphus - Ultraworker")
expect(capturedAgent).not.toContain("\u200B")
})
test("inherits tools from resolved message info when reinjecting", async () => {
// given
let capturedTools: Record<string, boolean> | undefined
@@ -20,6 +20,7 @@ import { isSqliteBackend } from "../../shared/opencode-storage-detection"
import {
getAgentConfigKey,
normalizeAgentForPromptKey,
stripAgentListSortPrefix,
} from "../../shared/agent-display-names"
import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate"
@@ -131,7 +132,8 @@ export async function injectContinuation(args: {
}
const promptAgent = normalizeAgentForPromptKey(agentName)
const launchAgent = resolveRegisteredAgentName(agentName)
const resolvedAgent = resolveRegisteredAgentName(agentName)
const launchAgent = resolvedAgent ? stripAgentListSortPrefix(resolvedAgent) : resolvedAgent
if (promptAgent && skipAgents.some(s => getAgentConfigKey(s) === getAgentConfigKey(promptAgent))) {
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName })