From b0c570e0544b46734f244304ec849c4973b3a1f2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 11 Feb 2026 11:55:56 +0900 Subject: [PATCH] fix(subagent): remove permission.question=deny override that caused zombie sessions Child session creation was injecting permission: { question: 'deny' } which conflicted with OpenCode's child session permission handling, causing subagent sessions to hang with 0 messages after creation (zombie state). Remove the permission override from all session creators (BackgroundManager, sync-session-creator, call-omo-agent) and rely on prompt-level tool restrictions (tools.question=false) to maintain the intended policy. Closes #1711 --- src/features/background-agent/manager.test.ts | 8 +-- src/features/background-agent/manager.ts | 7 --- src/features/background-agent/spawner.test.ts | 7 +-- src/features/background-agent/spawner.ts | 7 --- .../spawner/background-session-creator.ts | 1 - src/features/background-agent/task-starter.ts | 1 - .../call-omo-agent/session-creator.test.ts | 50 +++++++++++++++++++ src/tools/call-omo-agent/session-creator.ts | 5 +- .../subagent-session-creator.test.ts | 47 +++++++++++++++++ .../subagent-session-creator.ts | 2 + .../subagent-session-prompter.ts | 1 + .../delegate-task/sync-session-creator.ts | 3 -- 12 files changed, 106 insertions(+), 33 deletions(-) create mode 100644 src/tools/call-omo-agent/session-creator.test.ts create mode 100644 src/tools/call-omo-agent/subagent-session-creator.test.ts diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index d67ae2ad..83afdae4 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -1520,7 +1520,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { }) describe("task transitions pending→running when slot available", () => { - test("should inherit parent session permission rules (and force deny question)", async () => { + test("does not override parent session permission when creating child session", async () => { // given const createCalls: any[] = [] const parentPermission = [ @@ -1562,11 +1562,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { // then expect(createCalls).toHaveLength(1) - const permission = createCalls[0]?.body?.permission - expect(permission).toEqual([ - { permission: "plan_enter", action: "deny", pattern: "*" }, - { permission: "question", action: "deny", pattern: "*" }, - ]) + expect(createCalls[0]?.body?.permission).toBeUndefined() }) test("should transition first task to running immediately", async () => { diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 7872bebb..dad9dca2 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -236,17 +236,10 @@ export class BackgroundManager { const parentDirectory = parentSession?.data?.directory ?? this.directory log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`) - const inheritedPermission = (parentSession as any)?.data?.permission - const permissionRules = Array.isArray(inheritedPermission) - ? inheritedPermission.filter((r: any) => r?.permission !== "question") - : [] - permissionRules.push({ permission: "question", action: "deny" as const, pattern: "*" }) - const createResult = await this.client.session.create({ body: { parentID: input.parentSessionID, title: `${input.description} (@${input.agent} subagent)`, - permission: permissionRules, } as any, query: { directory: parentDirectory, diff --git a/src/features/background-agent/spawner.test.ts b/src/features/background-agent/spawner.test.ts index 334f3762..54f2fa00 100644 --- a/src/features/background-agent/spawner.test.ts +++ b/src/features/background-agent/spawner.test.ts @@ -3,7 +3,7 @@ import { describe, test, expect } from "bun:test" import { createTask, startTask } from "./spawner" describe("background-agent spawner.startTask", () => { - test("should inherit parent session permission rules (and force deny question)", async () => { + test("does not override parent session permission rules when creating child session", async () => { //#given const createCalls: any[] = [] const parentPermission = [ @@ -57,9 +57,6 @@ describe("background-agent spawner.startTask", () => { //#then expect(createCalls).toHaveLength(1) - expect(createCalls[0]?.body?.permission).toEqual([ - { permission: "plan_enter", action: "deny", pattern: "*" }, - { permission: "question", action: "deny", pattern: "*" }, - ]) + expect(createCalls[0]?.body?.permission).toBeUndefined() }) }) diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index 1b6773fb..e9256eca 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -58,17 +58,10 @@ export async function startTask( const parentDirectory = parentSession?.data?.directory ?? directory log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`) - const inheritedPermission = (parentSession as any)?.data?.permission - const permissionRules = Array.isArray(inheritedPermission) - ? inheritedPermission.filter((r: any) => r?.permission !== "question") - : [] - permissionRules.push({ permission: "question", action: "deny" as const, pattern: "*" }) - const createResult = await client.session.create({ body: { parentID: input.parentSessionID, title: `Background: ${input.description}`, - permission: permissionRules, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, query: { diff --git a/src/features/background-agent/spawner/background-session-creator.ts b/src/features/background-agent/spawner/background-session-creator.ts index 36aec443..9b27d869 100644 --- a/src/features/background-agent/spawner/background-session-creator.ts +++ b/src/features/background-agent/spawner/background-session-creator.ts @@ -15,7 +15,6 @@ export async function createBackgroundSession(options: { const body = { parentID: input.parentSessionID, title: `Background: ${input.description}`, - permission: [{ permission: "question", action: "deny" as const, pattern: "*" }], } const createResult = await client.session diff --git a/src/features/background-agent/task-starter.ts b/src/features/background-agent/task-starter.ts index 498f317d..9af87bdd 100644 --- a/src/features/background-agent/task-starter.ts +++ b/src/features/background-agent/task-starter.ts @@ -69,7 +69,6 @@ export async function startQueuedTask(args: { body: { parentID: input.parentSessionID, title: `${input.description} (@${input.agent} subagent)`, - permission: [{ permission: "question", action: "deny" as const, pattern: "*" }], } as any, query: { directory: parentDirectory, diff --git a/src/tools/call-omo-agent/session-creator.test.ts b/src/tools/call-omo-agent/session-creator.test.ts new file mode 100644 index 00000000..db231651 --- /dev/null +++ b/src/tools/call-omo-agent/session-creator.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test" + +import { createOrGetSession } from "./session-creator" +import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state" + +describe("call-omo-agent createOrGetSession", () => { + test("creates child session without overriding permission and tracks it as subagent session", async () => { + // given + _resetForTesting() + + const createCalls: Array = [] + const ctx = { + directory: "/project", + client: { + session: { + get: async () => ({ data: { directory: "/parent" } }), + create: async (args: unknown) => { + createCalls.push(args) + return { data: { id: "ses_child" } } + }, + }, + }, + } + + const toolContext = { + sessionID: "ses_parent", + messageID: "msg_parent", + agent: "sisyphus", + abort: new AbortController().signal, + } + + const args = { + description: "test", + prompt: "hello", + subagent_type: "explore", + run_in_background: true, + } + + // when + const result = await createOrGetSession(args as any, toolContext as any, ctx as any) + + // then + expect(result).toEqual({ sessionID: "ses_child", isNew: true }) + expect(createCalls).toHaveLength(1) + const createBody = (createCalls[0] as any)?.body + expect(createBody?.parentID).toBe("ses_parent") + expect(createBody?.permission).toBeUndefined() + expect(subagentSessions.has("ses_child")).toBe(true) + }) +}) diff --git a/src/tools/call-omo-agent/session-creator.ts b/src/tools/call-omo-agent/session-creator.ts index 64aa664d..37afad3f 100644 --- a/src/tools/call-omo-agent/session-creator.ts +++ b/src/tools/call-omo-agent/session-creator.ts @@ -1,5 +1,6 @@ import type { CallOmoAgentArgs } from "./types" import type { PluginInput } from "@opencode-ai/plugin" +import { subagentSessions } from "../../features/claude-code-session-state" import { log } from "../../shared" export async function createOrGetSession( @@ -38,9 +39,6 @@ export async function createOrGetSession( body: { parentID: toolContext.sessionID, title: `${args.description} (@${args.subagent_type} subagent)`, - permission: [ - { permission: "question", action: "deny" as const, pattern: "*" }, - ], } as any, query: { directory: parentDirectory, @@ -65,6 +63,7 @@ Original error: ${createResult.error}`) const sessionID = createResult.data.id log(`[call_omo_agent] Created session: ${sessionID}`) + subagentSessions.add(sessionID) return { sessionID, isNew: true } } } diff --git a/src/tools/call-omo-agent/subagent-session-creator.test.ts b/src/tools/call-omo-agent/subagent-session-creator.test.ts new file mode 100644 index 00000000..bacf60f4 --- /dev/null +++ b/src/tools/call-omo-agent/subagent-session-creator.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "bun:test" + +import { resolveOrCreateSessionId } from "./subagent-session-creator" +import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state" + +describe("call-omo-agent resolveOrCreateSessionId", () => { + test("tracks newly created child session as subagent session", async () => { + // given + _resetForTesting() + + const createCalls: Array = [] + const ctx = { + directory: "/project", + client: { + session: { + get: async () => ({ data: { directory: "/parent" } }), + create: async (args: unknown) => { + createCalls.push(args) + return { data: { id: "ses_child_sync" } } + }, + }, + }, + } + + const args = { + description: "sync test", + prompt: "hello", + subagent_type: "explore", + run_in_background: false, + } + + const toolContext = { + sessionID: "ses_parent", + messageID: "msg_parent", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when + const result = await resolveOrCreateSessionId(ctx as any, args as any, toolContext as any) + + // then + expect(result).toEqual({ ok: true, sessionID: "ses_child_sync" }) + expect(createCalls).toHaveLength(1) + expect(subagentSessions.has("ses_child_sync")).toBe(true) + }) +}) diff --git a/src/tools/call-omo-agent/subagent-session-creator.ts b/src/tools/call-omo-agent/subagent-session-creator.ts index 908060da..cd637d23 100644 --- a/src/tools/call-omo-agent/subagent-session-creator.ts +++ b/src/tools/call-omo-agent/subagent-session-creator.ts @@ -1,5 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" import { log } from "../../shared" +import { subagentSessions } from "../../features/claude-code-session-state" import type { CallOmoAgentArgs } from "./types" import type { ToolContextWithMetadata } from "./tool-context-with-metadata" @@ -63,5 +64,6 @@ Original error: ${createResult.error}`, const sessionID = createResult.data.id log(`[call_omo_agent] Created session: ${sessionID}`) + subagentSessions.add(sessionID) return { ok: true, sessionID } } diff --git a/src/tools/call-omo-agent/subagent-session-prompter.ts b/src/tools/call-omo-agent/subagent-session-prompter.ts index ce8e3796..286bfc99 100644 --- a/src/tools/call-omo-agent/subagent-session-prompter.ts +++ b/src/tools/call-omo-agent/subagent-session-prompter.ts @@ -13,6 +13,7 @@ export async function promptSubagentSession( tools: { ...getAgentToolRestrictions(options.agent), task: false, + question: false, }, parts: [{ type: "text", text: options.prompt }], }, diff --git a/src/tools/delegate-task/sync-session-creator.ts b/src/tools/delegate-task/sync-session-creator.ts index 400a6da0..ed9b4c74 100644 --- a/src/tools/delegate-task/sync-session-creator.ts +++ b/src/tools/delegate-task/sync-session-creator.ts @@ -13,9 +13,6 @@ export async function createSyncSession( body: { parentID: input.parentSessionID, title: `${input.description} (@${input.agentToUse} subagent)`, - permission: [ - { permission: "question", action: "deny" as const, pattern: "*" }, - ], } as any, query: { directory: parentDirectory,