From 25d183fbe837b4fcf9fe5a084f6a7948bed94d93 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 6 May 2026 17:41:41 +0900 Subject: [PATCH] fix(agent): sanitize backslash/quote from agent name in launch() and background-executor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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\' --- bun.lock | 44 ++++++------- src/features/background-agent/manager.test.ts | 63 +++++++++++++++++++ src/features/background-agent/manager.ts | 6 ++ src/shared/agent-display-names.test.ts | 4 ++ src/shared/agent-display-names.ts | 3 +- .../background-executor.test.ts | 35 +++++++++++ .../call-omo-agent/background-executor.ts | 3 +- 7 files changed, 134 insertions(+), 24 deletions(-) diff --git a/bun.lock b/bun.lock index 7498f0c8e..a2dbefafc 100644 --- a/bun.lock +++ b/bun.lock @@ -30,17 +30,17 @@ "zod": "^4.3.0", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.17.13", - "oh-my-opencode-darwin-x64": "3.17.13", - "oh-my-opencode-darwin-x64-baseline": "3.17.13", - "oh-my-opencode-linux-arm64": "3.17.13", - "oh-my-opencode-linux-arm64-musl": "3.17.13", - "oh-my-opencode-linux-x64": "3.17.13", - "oh-my-opencode-linux-x64-baseline": "3.17.13", - "oh-my-opencode-linux-x64-musl": "3.17.13", - "oh-my-opencode-linux-x64-musl-baseline": "3.17.13", - "oh-my-opencode-windows-x64": "3.17.13", - "oh-my-opencode-windows-x64-baseline": "3.17.13", + "oh-my-opencode-darwin-arm64": "3.17.15", + "oh-my-opencode-darwin-x64": "3.17.15", + "oh-my-opencode-darwin-x64-baseline": "3.17.15", + "oh-my-opencode-linux-arm64": "3.17.15", + "oh-my-opencode-linux-arm64-musl": "3.17.15", + "oh-my-opencode-linux-x64": "3.17.15", + "oh-my-opencode-linux-x64-baseline": "3.17.15", + "oh-my-opencode-linux-x64-musl": "3.17.15", + "oh-my-opencode-linux-x64-musl-baseline": "3.17.15", + "oh-my-opencode-windows-x64": "3.17.15", + "oh-my-opencode-windows-x64-baseline": "3.17.15", }, "peerDependencies": { "zod": "^4.0.0", @@ -241,27 +241,27 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.17.13", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-5n+eEhh7tfE62BT8cC2XjTXubcXQOEGNTCWSOZ7C1mrE9E5xbvPb/ctWLrGTIk9rkl5+MJW3IZ7HLniAphxCEA=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.17.15", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-S0BpJVAwBcwSjd3Y5zE9mb6fKrRf2be1jIYnlifpbCyEI9yiludzuqQ9WKJstZQYD7HJpPlAMPIbJwojSl95sw=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.17.13", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-PMy2e+gH7C3w1BHbtvHWKajxCbP2srV9N+Rv5nJ4Xh1SuSUO+8U+DlASsbCo6ArskP3IyJVRyykm34nLdkcu6w=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.17.15", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-of8+u/jCobddh1aGTGugLyCcDtib76ZmzNuFEE7HT/G3pYthiJzxb9t2qPBKwbYmILNeJJjov7VL5XLD08IVnA=="], - "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.17.13", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-n1qw52wVqoqpoDrH/AnHq4YznzV8nPbrUtm/18aChklrLjGJafn0oVnhUVFfceqKKqsFtU6A4OgkRgTK46vhUQ=="], + "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.17.15", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Jc03G9drhyawG9GsAQO242Ct36qyn0LdJJuXHZ8ULYQ1+fsVFbr/h6OqHLh1JfKkzXRZngDHbyGJ0mqfZsXhmA=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.17.13", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-JYpDFUbMj9NZRODA+cP63+hgi+G+uBp6Tqjy/FyK/xi0kIs+yIaR4dYni515mJ4SMfErPkDue2m0S/ljawTV3A=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.17.15", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-k0I8CH7UFVmJPA/qj95VVlQc4kRKGTho1Lm6sz1dI58GdzXesclGkBMkXyRH45cja8zDKKHdZJipcQTel/vbZw=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.17.13", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-wYHvdf0C+8gyw28NOhGAdR6UK8BIeL+BDTq6fC9FDmDY4xqQN8YiwGKj/vYChQJfPrI9fKhaZF++lIiJn23DpQ=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.17.15", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-I1wAoysz8E4Iym8wTFzue/hKFRD3QnkIyoHtB9j/4kFD+z5NzxDvBQ7q+beYjErGzyCC1qpM6/HIbBuVLfOvpQ=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.17.13", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-3/kHPfRuAX7blK1onPEBn72TRM1yoWooVRMYNRfufXJz1lcCq+Oxgn2PfEbEit84XKWpGa9RGJ+m3Oc0Pe9seQ=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.17.15", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-saXRRWHt3b9xJ3zELCvTxSR75j+nJ/ZkDukTNNWjodeOvESzxe9yc+7Oi7XRUOIGZ2zqtjTPVc8py15ifTW3mA=="], - "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.17.13", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-nl07YZIpPhh6sVc86YcO8bKVhUlh82RkmldzMBM/xJSGqoj2vLwq+Ab9LxmLqPZVh8BIzPhWf4qgJhLhy9UA3g=="], + "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.17.15", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-a3QxM9w0UQ7Wk5CDWwKkerTLYlBVGbXDvIgKW+TDtMEfQWaSnKTT27mjv9o5E5PQKYxUhgZaMnIutzX/LuIsmw=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.17.13", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-AfHEN9pp8a67N/sVglHSNADRHKag/4IstOtwCzhTqQw/T2NIvjuQGYvTLUzPcbyP1OH3V/TbpN+sEg/FUaWAkA=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.17.15", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-xrzbO5iThuox8jbJY3IBst5EO2BvmNtKE6ScgyE8EonSvsLa53l35MI0S4y5iS208LB4E3o1uZJFdK4c13xaPQ=="], - "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.17.13", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-d/xD1lod2rWq0/pgwECG45/yxO7smErlwFca2JDwTCgwCksDpl/aw/I0Js05a6cJaix+02pyPKhet/StxcLyzQ=="], + "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.17.15", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-/bbQK5w2s4DVX6kzT6n670xR8sqaxr8TLBFJz1ZaygQ8S5kIo+owIM3CIOZG9HfuwUBznOhDnNvTbBkDzeZlIw=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.17.13", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-HSPsItIqAIVqodIPhr6sL8+DNam53bJSTivrVQb7+S5yeIeQ6WkM22E7EkvZmuP0jGMYwWr6jZMgR8or1C2PfA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.17.15", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-+CoU4oWktRbzUusyAIQCIKprGNKmoZeziCqbPxGXgtjwyMy+1hy1K4Ow+v1bzCgrXNBbepKeDfKr7EoafxdHkQ=="], - "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.17.13", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-NjrXpTZcBTYZ490vt+6YJCTc3aaeW+4qNQ+NQ8o4eUH17ehnCUl/1SwORLx7DqkbxIM0JFkZm1oypTkEn0OsXQ=="], + "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.17.15", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-gvxS4ZpY5qPo0HdclInF0I3VZL3s88UUELXf2GVKbsY7OJ9kT+itB4OtNcWJBiP36dBQnAX7BZqEWRJk9iJwPA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 04f261dd9..c8d66158c 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -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 = { diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 688ad0ec7..2500ae9f6 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -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 { diff --git a/src/shared/agent-display-names.test.ts b/src/shared/agent-display-names.test.ts index 92798275d..3a1bc98dd 100644 --- a/src/shared/agent-display-names.test.ts +++ b/src/shared/agent-display-names.test.ts @@ -214,6 +214,10 @@ describe("stripAgentListSortPrefix", () => { it("strips legacy zero-width sort prefixes baked into v3.14.0–v3.16.0 sessions", () => { expect(stripAgentListSortPrefix("\u200B\u200BHephaestus - Deep Agent")).toBe("Hephaestus - Deep Agent") }) + + it("strips leading and trailing wrapper characters after sort prefix removal", () => { + expect(stripAgentListSortPrefix("\\Hephaestus - Deep Agent\\")).toBe("Hephaestus - Deep Agent") + }) }) describe("normalizeAgentForPrompt", () => { diff --git a/src/shared/agent-display-names.ts b/src/shared/agent-display-names.ts index ac8f546f0..9a7f9c517 100644 --- a/src/shared/agent-display-names.ts +++ b/src/shared/agent-display-names.ts @@ -28,13 +28,14 @@ export const AGENT_DISPLAY_NAMES: Record = { const INVISIBLE_AGENT_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g const VISIBLE_AGENT_LIST_SORT_PREFIX_REGEX = /^\d+\|/ +const AGENT_WRAPPER_CHARS_REGEX = /^[\\/"']+|[\\/"']+$/g export function stripInvisibleAgentCharacters(agentName: string): string { return agentName.replace(INVISIBLE_AGENT_CHARACTERS_REGEX, "") } export function stripAgentListSortPrefix(agentName: string): string { - return stripInvisibleAgentCharacters(agentName).replace(VISIBLE_AGENT_LIST_SORT_PREFIX_REGEX, "") + return stripInvisibleAgentCharacters(agentName).replace(VISIBLE_AGENT_LIST_SORT_PREFIX_REGEX, "").replace(AGENT_WRAPPER_CHARS_REGEX, "") } /** diff --git a/src/tools/call-omo-agent/background-executor.test.ts b/src/tools/call-omo-agent/background-executor.test.ts index 243dc15b8..9e526e82d 100644 --- a/src/tools/call-omo-agent/background-executor.test.ts +++ b/src/tools/call-omo-agent/background-executor.test.ts @@ -1,5 +1,11 @@ /// import { describe, test, expect, mock } from "bun:test" +mock.module("../../shared/frontmatter", () => ({ + parseFrontmatter: () => ({ frontmatter: {}, content: "" }), +})) +mock.module("js-yaml", () => ({ + load: () => ({}), +})) import type { BackgroundManager } from "../../features/background-agent" import type { PluginInput } from "@opencode-ai/plugin" import { executeBackground } from "./background-executor" @@ -100,6 +106,35 @@ describe("executeBackground", () => { expect(launchArgs.fallbackChain).toEqual(fallbackChain) }) + test("sanitizes subagent_type before passing to background manager launch", async () => { + //#given + const wrappedArgs = { + ...testArgs, + subagent_type: "\\hephaestus\\", + } + launchMock.mockResolvedValueOnce({ + id: "test-task-id", + sessionId: "sub-session", + description: "Test task", + agent: "hephaestus", + status: "pending", + }) + + //#when + await executeBackground(wrappedArgs, testContext, mockManager, mockClient) + + //#then + const latestCall = [...launchMock.mock.calls].pop() + if (!latestCall) { + throw new Error("Expected background manager launch to be called") + } + const launchArgs = latestCall[0] + if (!launchArgs) { + throw new Error("Expected launch arguments") + } + expect(launchArgs.agent).toBe("hephaestus") + }) + test("keeps launched background task alive when parent aborts before session id resolves", async () => { //#given - parent abort after launch should stop waiting, not fail the background task const abortController = new AbortController() diff --git a/src/tools/call-omo-agent/background-executor.ts b/src/tools/call-omo-agent/background-executor.ts index 0483abc64..cae87c986 100644 --- a/src/tools/call-omo-agent/background-executor.ts +++ b/src/tools/call-omo-agent/background-executor.ts @@ -8,6 +8,7 @@ import { resolveMessageContext } from "../../features/hook-message-injector" import { getSessionAgent } from "../../features/claude-code-session-state" import { getMessageDir } from "./message-dir" import { getSessionTools } from "../../shared/session-tools-store" +import { sanitizeSubagentType } from "../delegate-task/subagent-discovery" export async function executeBackground( args: CallOmoAgentArgs, @@ -47,7 +48,7 @@ export async function executeBackground( const task = await manager.launch({ description: args.description, prompt: args.prompt, - agent: args.subagent_type, + agent: sanitizeSubagentType(args.subagent_type), parentSessionId: toolContext.sessionID, parentMessageId: toolContext.messageID, parentAgent,