YeonGyu-Kim b0c570e054 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
2026-02-11 13:52:20 +09:00

191 lines
5.8 KiB
TypeScript

import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } from "../../shared"
import { isInsideTmux } from "../../shared/tmux"
import { subagentSessions } from "../claude-code-session-state"
import { getTaskToastManager } from "../task-toast-manager"
import type { BackgroundTask } from "./types"
import type { LaunchInput } from "./types"
import type { ConcurrencyManager } from "./concurrency"
import type { OpencodeClient } from "./opencode-client"
type QueueItem = {
task: BackgroundTask
input: LaunchInput
}
type ModelRef = { providerID: string; modelID: string }
export async function startQueuedTask(args: {
item: QueueItem
client: OpencodeClient
defaultDirectory: string
tmuxEnabled: boolean
onSubagentSessionCreated?: (event: { sessionID: string; parentID: string; title: string }) => Promise<void>
startPolling: () => void
getConcurrencyKeyFromInput: (input: LaunchInput) => string
concurrencyManager: ConcurrencyManager
findBySession: (sessionID: string) => BackgroundTask | undefined
markForNotification: (task: BackgroundTask) => void
cleanupPendingByParent: (task: BackgroundTask) => void
notifyParentSession: (task: BackgroundTask) => Promise<void>
}): Promise<void> {
const {
item,
client,
defaultDirectory,
tmuxEnabled,
onSubagentSessionCreated,
startPolling,
getConcurrencyKeyFromInput,
concurrencyManager,
findBySession,
markForNotification,
cleanupPendingByParent,
notifyParentSession,
} = args
const { task, input } = item
log("[background-agent] Starting task:", {
taskId: task.id,
agent: input.agent,
model: input.model,
})
const concurrencyKey = getConcurrencyKeyFromInput(input)
const parentSession = await client.session.get({
path: { id: input.parentSessionID },
}).catch((err) => {
log(`[background-agent] Failed to get parent session: ${err}`)
return null
})
const parentDirectory = parentSession?.data?.directory ?? defaultDirectory
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
const createResult = await client.session.create({
body: {
parentID: input.parentSessionID,
title: `${input.description} (@${input.agent} subagent)`,
} as any,
query: {
directory: parentDirectory,
},
})
if (createResult.error) {
throw new Error(`Failed to create background session: ${createResult.error}`)
}
if (!createResult.data?.id) {
throw new Error("Failed to create background session: API returned no session ID")
}
const sessionID = createResult.data.id
subagentSessions.add(sessionID)
log("[background-agent] tmux callback check", {
hasCallback: !!onSubagentSessionCreated,
tmuxEnabled,
isInsideTmux: isInsideTmux(),
sessionID,
parentID: input.parentSessionID,
})
if (onSubagentSessionCreated && tmuxEnabled && isInsideTmux()) {
log("[background-agent] Invoking tmux callback NOW", { sessionID })
await onSubagentSessionCreated({
sessionID,
parentID: input.parentSessionID,
title: input.description,
}).catch((err) => {
log("[background-agent] Failed to spawn tmux pane:", err)
})
log("[background-agent] tmux callback completed, waiting 200ms")
await new Promise<void>((resolve) => {
setTimeout(() => resolve(), 200)
})
} else {
log("[background-agent] SKIP tmux callback - conditions not met")
}
task.status = "running"
task.startedAt = new Date()
task.sessionID = sessionID
task.progress = {
toolCalls: 0,
lastUpdate: new Date(),
}
task.concurrencyKey = concurrencyKey
task.concurrencyGroup = concurrencyKey
startPolling()
log("[background-agent] Launching task:", { taskId: task.id, sessionID, agent: input.agent })
const toastManager = getTaskToastManager()
if (toastManager) {
toastManager.updateTask(task.id, "running")
}
log("[background-agent] Calling prompt (fire-and-forget) for launch with:", {
sessionID,
agent: input.agent,
model: input.model,
hasSkillContent: !!input.skillContent,
promptLength: input.prompt.length,
})
const launchModel: ModelRef | undefined = input.model
? { providerID: input.model.providerID, modelID: input.model.modelID }
: undefined
const launchVariant = input.model?.variant
promptWithModelSuggestionRetry(client, {
path: { id: sessionID },
body: {
agent: input.agent,
...(launchModel ? { model: launchModel } : {}),
...(launchVariant ? { variant: launchVariant } : {}),
system: input.skillContent,
tools: {
...getAgentToolRestrictions(input.agent),
task: false,
call_omo_agent: true,
question: false,
},
parts: [{ type: "text", text: input.prompt }],
},
}).catch((error) => {
log("[background-agent] promptAsync error:", error)
const existingTask = findBySession(sessionID)
if (!existingTask) return
existingTask.status = "interrupt"
const errorMessage = error instanceof Error ? error.message : String(error)
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.`
} else {
existingTask.error = errorMessage
}
existingTask.completedAt = new Date()
if (existingTask.concurrencyKey) {
concurrencyManager.release(existingTask.concurrencyKey)
existingTask.concurrencyKey = undefined
}
client.session.abort({
path: { id: sessionID },
}).catch(() => {})
markForNotification(existingTask)
cleanupPendingByParent(existingTask)
notifyParentSession(existingTask).catch((err) => {
log("[background-agent] Failed to notify on error:", err)
})
})
}