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:
@@ -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`, {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user