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
This commit is contained in:
parent
fd99a29d6e
commit
b0c570e054
@ -1520,7 +1520,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("task transitions pending→running when slot available", () => {
|
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
|
// given
|
||||||
const createCalls: any[] = []
|
const createCalls: any[] = []
|
||||||
const parentPermission = [
|
const parentPermission = [
|
||||||
@ -1562,11 +1562,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(createCalls).toHaveLength(1)
|
expect(createCalls).toHaveLength(1)
|
||||||
const permission = createCalls[0]?.body?.permission
|
expect(createCalls[0]?.body?.permission).toBeUndefined()
|
||||||
expect(permission).toEqual([
|
|
||||||
{ permission: "plan_enter", action: "deny", pattern: "*" },
|
|
||||||
{ permission: "question", action: "deny", pattern: "*" },
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should transition first task to running immediately", async () => {
|
test("should transition first task to running immediately", async () => {
|
||||||
|
|||||||
@ -236,17 +236,10 @@ export class BackgroundManager {
|
|||||||
const parentDirectory = parentSession?.data?.directory ?? this.directory
|
const parentDirectory = parentSession?.data?.directory ?? this.directory
|
||||||
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
|
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({
|
const createResult = await this.client.session.create({
|
||||||
body: {
|
body: {
|
||||||
parentID: input.parentSessionID,
|
parentID: input.parentSessionID,
|
||||||
title: `${input.description} (@${input.agent} subagent)`,
|
title: `${input.description} (@${input.agent} subagent)`,
|
||||||
permission: permissionRules,
|
|
||||||
} as any,
|
} as any,
|
||||||
query: {
|
query: {
|
||||||
directory: parentDirectory,
|
directory: parentDirectory,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { describe, test, expect } from "bun:test"
|
|||||||
import { createTask, startTask } from "./spawner"
|
import { createTask, startTask } from "./spawner"
|
||||||
|
|
||||||
describe("background-agent spawner.startTask", () => {
|
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
|
//#given
|
||||||
const createCalls: any[] = []
|
const createCalls: any[] = []
|
||||||
const parentPermission = [
|
const parentPermission = [
|
||||||
@ -57,9 +57,6 @@ describe("background-agent spawner.startTask", () => {
|
|||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(createCalls).toHaveLength(1)
|
expect(createCalls).toHaveLength(1)
|
||||||
expect(createCalls[0]?.body?.permission).toEqual([
|
expect(createCalls[0]?.body?.permission).toBeUndefined()
|
||||||
{ permission: "plan_enter", action: "deny", pattern: "*" },
|
|
||||||
{ permission: "question", action: "deny", pattern: "*" },
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -58,17 +58,10 @@ export async function startTask(
|
|||||||
const parentDirectory = parentSession?.data?.directory ?? directory
|
const parentDirectory = parentSession?.data?.directory ?? directory
|
||||||
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
|
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({
|
const createResult = await client.session.create({
|
||||||
body: {
|
body: {
|
||||||
parentID: input.parentSessionID,
|
parentID: input.parentSessionID,
|
||||||
title: `Background: ${input.description}`,
|
title: `Background: ${input.description}`,
|
||||||
permission: permissionRules,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} as any,
|
} as any,
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@ -15,7 +15,6 @@ export async function createBackgroundSession(options: {
|
|||||||
const body = {
|
const body = {
|
||||||
parentID: input.parentSessionID,
|
parentID: input.parentSessionID,
|
||||||
title: `Background: ${input.description}`,
|
title: `Background: ${input.description}`,
|
||||||
permission: [{ permission: "question", action: "deny" as const, pattern: "*" }],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createResult = await client.session
|
const createResult = await client.session
|
||||||
|
|||||||
@ -69,7 +69,6 @@ export async function startQueuedTask(args: {
|
|||||||
body: {
|
body: {
|
||||||
parentID: input.parentSessionID,
|
parentID: input.parentSessionID,
|
||||||
title: `${input.description} (@${input.agent} subagent)`,
|
title: `${input.description} (@${input.agent} subagent)`,
|
||||||
permission: [{ permission: "question", action: "deny" as const, pattern: "*" }],
|
|
||||||
} as any,
|
} as any,
|
||||||
query: {
|
query: {
|
||||||
directory: parentDirectory,
|
directory: parentDirectory,
|
||||||
|
|||||||
50
src/tools/call-omo-agent/session-creator.test.ts
Normal file
50
src/tools/call-omo-agent/session-creator.test.ts
Normal file
@ -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<unknown> = []
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import type { CallOmoAgentArgs } from "./types"
|
import type { CallOmoAgentArgs } from "./types"
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { subagentSessions } from "../../features/claude-code-session-state"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
|
|
||||||
export async function createOrGetSession(
|
export async function createOrGetSession(
|
||||||
@ -38,9 +39,6 @@ export async function createOrGetSession(
|
|||||||
body: {
|
body: {
|
||||||
parentID: toolContext.sessionID,
|
parentID: toolContext.sessionID,
|
||||||
title: `${args.description} (@${args.subagent_type} subagent)`,
|
title: `${args.description} (@${args.subagent_type} subagent)`,
|
||||||
permission: [
|
|
||||||
{ permission: "question", action: "deny" as const, pattern: "*" },
|
|
||||||
],
|
|
||||||
} as any,
|
} as any,
|
||||||
query: {
|
query: {
|
||||||
directory: parentDirectory,
|
directory: parentDirectory,
|
||||||
@ -65,6 +63,7 @@ Original error: ${createResult.error}`)
|
|||||||
|
|
||||||
const sessionID = createResult.data.id
|
const sessionID = createResult.data.id
|
||||||
log(`[call_omo_agent] Created session: ${sessionID}`)
|
log(`[call_omo_agent] Created session: ${sessionID}`)
|
||||||
|
subagentSessions.add(sessionID)
|
||||||
return { sessionID, isNew: true }
|
return { sessionID, isNew: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/tools/call-omo-agent/subagent-session-creator.test.ts
Normal file
47
src/tools/call-omo-agent/subagent-session-creator.test.ts
Normal file
@ -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<unknown> = []
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
|
import { subagentSessions } from "../../features/claude-code-session-state"
|
||||||
import type { CallOmoAgentArgs } from "./types"
|
import type { CallOmoAgentArgs } from "./types"
|
||||||
import type { ToolContextWithMetadata } from "./tool-context-with-metadata"
|
import type { ToolContextWithMetadata } from "./tool-context-with-metadata"
|
||||||
|
|
||||||
@ -63,5 +64,6 @@ Original error: ${createResult.error}`,
|
|||||||
|
|
||||||
const sessionID = createResult.data.id
|
const sessionID = createResult.data.id
|
||||||
log(`[call_omo_agent] Created session: ${sessionID}`)
|
log(`[call_omo_agent] Created session: ${sessionID}`)
|
||||||
|
subagentSessions.add(sessionID)
|
||||||
return { ok: true, sessionID }
|
return { ok: true, sessionID }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export async function promptSubagentSession(
|
|||||||
tools: {
|
tools: {
|
||||||
...getAgentToolRestrictions(options.agent),
|
...getAgentToolRestrictions(options.agent),
|
||||||
task: false,
|
task: false,
|
||||||
|
question: false,
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: options.prompt }],
|
parts: [{ type: "text", text: options.prompt }],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,9 +13,6 @@ export async function createSyncSession(
|
|||||||
body: {
|
body: {
|
||||||
parentID: input.parentSessionID,
|
parentID: input.parentSessionID,
|
||||||
title: `${input.description} (@${input.agentToUse} subagent)`,
|
title: `${input.description} (@${input.agentToUse} subagent)`,
|
||||||
permission: [
|
|
||||||
{ permission: "question", action: "deny" as const, pattern: "*" },
|
|
||||||
],
|
|
||||||
} as any,
|
} as any,
|
||||||
query: {
|
query: {
|
||||||
directory: parentDirectory,
|
directory: parentDirectory,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user