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:
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -28,13 +28,14 @@ export const AGENT_DISPLAY_NAMES: Record<string, string> = {
|
||||
|
||||
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, "")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/// <reference types="bun-types" />
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user