From b4aac44f0d620ecfbf4ac8b79405dc34fede15a9 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Mar 2026 02:22:21 +0900 Subject: [PATCH] feat(background-agent): add subagent spawn context resolver Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../background-agent/subagent-spawn-limits.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/features/background-agent/subagent-spawn-limits.ts diff --git a/src/features/background-agent/subagent-spawn-limits.ts b/src/features/background-agent/subagent-spawn-limits.ts new file mode 100644 index 000000000..2b0068102 --- /dev/null +++ b/src/features/background-agent/subagent-spawn-limits.ts @@ -0,0 +1,79 @@ +import type { BackgroundTaskConfig } from "../../config/schema" +import type { OpencodeClient } from "./constants" + +export const DEFAULT_MAX_SUBAGENT_DEPTH = 3 +export const DEFAULT_MAX_ROOT_DESCENDANTS = 50 + +export interface SubagentSpawnContext { + rootSessionID: string + parentDepth: number + childDepth: number +} + +export function getMaxSubagentDepth(config?: BackgroundTaskConfig): number { + return config?.maxDepth ?? DEFAULT_MAX_SUBAGENT_DEPTH +} + +export function getMaxRootDescendants(config?: BackgroundTaskConfig): number { + return config?.maxDescendants ?? DEFAULT_MAX_ROOT_DESCENDANTS +} + +export async function resolveSubagentSpawnContext( + client: OpencodeClient, + parentSessionID: string +): Promise { + const visitedSessionIDs = new Set() + let rootSessionID = parentSessionID + let currentSessionID = parentSessionID + let parentDepth = 0 + + while (true) { + if (visitedSessionIDs.has(currentSessionID)) { + throw new Error(`Detected a session parent cycle while resolving ${parentSessionID}`) + } + + visitedSessionIDs.add(currentSessionID) + + const session = await client.session.get({ + path: { id: currentSessionID }, + }).catch(() => null) + + const nextParentSessionID = session?.data?.parentID + if (!nextParentSessionID) { + rootSessionID = currentSessionID + break + } + + currentSessionID = nextParentSessionID + parentDepth += 1 + } + + return { + rootSessionID, + parentDepth, + childDepth: parentDepth + 1, + } +} + +export function createSubagentDepthLimitError(input: { + childDepth: number + maxDepth: number + parentSessionID: string + rootSessionID: string +}): Error { + const { childDepth, maxDepth, parentSessionID, rootSessionID } = input + return new Error( + `Subagent spawn blocked: child depth ${childDepth} exceeds background_task.maxDepth=${maxDepth}. Parent session: ${parentSessionID}. Root session: ${rootSessionID}. Continue in an existing subagent session instead of spawning another.` + ) +} + +export function createSubagentDescendantLimitError(input: { + rootSessionID: string + descendantCount: number + maxDescendants: number +}): Error { + const { rootSessionID, descendantCount, maxDescendants } = input + return new Error( + `Subagent spawn blocked: root session ${rootSessionID} already has ${descendantCount} descendants, which meets background_task.maxDescendants=${maxDescendants}. Reuse an existing session instead of spawning another.` + ) +}