fix(agent): sanitize backslash/quote from agent name in launch() and background-executor

Agent names like \hephaestus\ cause 'Agent not found' errors because
sanitizeSubagentType() was only called in subagent-resolver.ts but not
in the direct manager.launch() path or background-executor.ts.

- manager.ts: strip leading/trailing backslash/quote chars from input.agent
  before validation so \hephaestus\ → hephaestus
- background-executor.ts: call sanitizeSubagentType(args.subagent_type)
  instead of passing raw value to manager.launch()
- agent-display-names.ts: reuse sanitizeSubagentType in stripAgentListSortPrefix
- Add unit tests for all three fix points

Fixes: sessions dying with 'Agent not found: \hephaestus\'
This commit is contained in:
YeonGyu-Kim
2026-05-06 17:41:41 +09:00
parent 6a341fcb8f
commit 25d183fbe8
7 changed files with 134 additions and 24 deletions
@@ -23,6 +23,12 @@ mock.module("../../shared/connected-providers-cache", () => ({
writeProviderModelsCache: () => {},
updateConnectedProvidersCache: () => {},
}))
mock.module("../../shared/frontmatter", () => ({
parseFrontmatter: () => ({ frontmatter: {}, content: "" }),
}))
mock.module("js-yaml", () => ({
load: () => ({}),
}))
mock.restore()
@@ -2447,6 +2453,63 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
expect(task.sessionId).toBeUndefined()
})
test("should sanitize wrapped agent names before task creation and queueing", async () => {
// given
const input = {
description: "Test task",
prompt: "Do something",
agent: "\\hephaestus\\",
parentSessionId: "parent-session",
parentMessageId: "parent-message",
}
// when
const task = await manager.launch(input)
const queueItem = getQueuesByKey(manager).values().next().value?.[0]
// then
expect(task.agent).toBe("hephaestus")
expect(getTaskMap(manager).get(task.id)?.agent).toBe("hephaestus")
expect(queueItem?.input.agent).toBe("hephaestus")
})
test("should sanitize slash and quote wrapped agent names before task creation and queueing", async () => {
// given
const input = {
description: "Test task",
prompt: "Do something",
agent: "\"/hephaestus/\"",
parentSessionId: "parent-session",
parentMessageId: "parent-message",
}
// when
const task = await manager.launch(input)
const queueItem = getQueuesByKey(manager).values().next().value?.[0]
// then
expect(task.agent).toBe("hephaestus")
expect(getTaskMap(manager).get(task.id)?.agent).toBe("hephaestus")
expect(queueItem?.input.agent).toBe("hephaestus")
})
test("should reject wrapper-only agent names after sanitization", async () => {
// given
const input = {
description: "Test task",
prompt: "Do something",
agent: "\\\"/'\\\"/",
parentSessionId: "parent-session",
parentMessageId: "parent-message",
}
// when
const result = manager.launch(input)
// then
await expect(result).rejects.toThrow("Agent parameter is required after sanitization")
})
test("should initialize attempt state for a newly launched task", async () => {
// given
const input = {
+6
View File
@@ -383,6 +383,12 @@ export class BackgroundManager {
throw new Error("Agent parameter is required")
}
input = { ...input, agent: input.agent.trim().replace(/^[\\/"']+|[\\/"']+$/g, "").trim() }
if (!input.agent) {
throw new Error("Agent parameter is required after sanitization")
}
const spawnReservation = await this.reserveSubagentSpawn(input.parentSessionId)
try {