diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 7ad6fea39..b0fffbf6b 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -2327,7 +2327,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { await expect(result).rejects.toThrow("background_task.maxDepth=3") }) - test("should block launches when maxDescendants is reached", async () => { + test("should ignore legacy maxDescendants config when launching multiple descendants", async () => { // given manager.shutdown() manager = new BackgroundManager( @@ -2354,10 +2354,10 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { const result = manager.launch(input) // then - await expect(result).rejects.toThrow("background_task.maxDescendants=1") + await expect(result).resolves.toBeDefined() }) - test("should consume descendant quota for reserved sync spawns", async () => { + test("should allow spawn assertions after reserveSubagentSpawn even with legacy maxDescendants config", async () => { // given manager.shutdown() manager = new BackgroundManager( @@ -2376,7 +2376,10 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { const result = manager.assertCanSpawn("session-root") // then - await expect(result).rejects.toThrow("background_task.maxDescendants=1") + await expect(result).resolves.toMatchObject({ + rootSessionID: "session-root", + childDepth: 1, + }) }) test("should fail closed when session lineage lookup fails", async () => { @@ -2407,7 +2410,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { const result = manager.launch(input) // then - await expect(result).rejects.toThrow("background_task.maxDescendants cannot be enforced safely") + await expect(result).rejects.toThrow("background_task.maxDepth cannot be enforced safely") }) test("should release descendant quota when queued task is cancelled before session starts", async () => { diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index a59ea9530..f418580c2 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -72,8 +72,6 @@ import { } from "./loop-detector" import { createSubagentDepthLimitError, - createSubagentDescendantLimitError, - getMaxRootSessionSpawnBudget, getMaxSubagentDepth, resolveSubagentSpawnContext, type SubagentSpawnContext, @@ -218,16 +216,6 @@ export class BackgroundManager { }) } - const maxRootSessionSpawnBudget = getMaxRootSessionSpawnBudget(this.config) - const descendantCount = this.rootDescendantCounts.get(spawnContext.rootSessionID) ?? 0 - if (descendantCount >= maxRootSessionSpawnBudget) { - throw createSubagentDescendantLimitError({ - rootSessionID: spawnContext.rootSessionID, - descendantCount, - maxDescendants: maxRootSessionSpawnBudget, - }) - } - return spawnContext } diff --git a/src/features/background-agent/subagent-spawn-limits.test.ts b/src/features/background-agent/subagent-spawn-limits.test.ts index e158c0dad..e3094c551 100644 --- a/src/features/background-agent/subagent-spawn-limits.test.ts +++ b/src/features/background-agent/subagent-spawn-limits.test.ts @@ -5,9 +5,6 @@ import { getMaxSubagentDepth, DEFAULT_MAX_SUBAGENT_DEPTH, createSubagentDepthLimitError, - createSubagentDescendantLimitError, - getMaxRootSessionSpawnBudget, - DEFAULT_MAX_ROOT_SESSION_SPAWN_BUDGET, } from "./subagent-spawn-limits" function createMockClient(sessionGet: OpencodeClient["session"]["get"]): OpencodeClient { @@ -62,7 +59,7 @@ describe("resolveSubagentSpawnContext", () => { const result = resolveSubagentSpawnContext(client, "parent-session") // then - await expect(result).rejects.toThrow(/background_task\.maxDescendants cannot be enforced safely.*lookup failed/) + await expect(result).rejects.toThrow(/background_task\.maxDepth cannot be enforced safely.*lookup failed/) }) }) @@ -77,7 +74,7 @@ describe("resolveSubagentSpawnContext", () => { const result = resolveSubagentSpawnContext(client, "parent-session") // then - await expect(result).rejects.toThrow(/background_task\.maxDescendants cannot be enforced safely.*No session data returned/) + await expect(result).rejects.toThrow(/background_task\.maxDepth cannot be enforced safely.*No session data returned/) }) }) @@ -209,20 +206,6 @@ describe("getMaxSubagentDepth", () => { }) }) -describe("getMaxRootSessionSpawnBudget", () => { - test("returns DEFAULT_MAX_ROOT_SESSION_SPAWN_BUDGET when no config", () => { - expect(getMaxRootSessionSpawnBudget()).toBe(DEFAULT_MAX_ROOT_SESSION_SPAWN_BUDGET) - }) - - test("returns config.maxDescendants when provided", () => { - expect(getMaxRootSessionSpawnBudget({ maxDescendants: 10 })).toBe(10) - }) - - test("default is 50", () => { - expect(DEFAULT_MAX_ROOT_SESSION_SPAWN_BUDGET).toBe(50) - }) -}) - describe("createSubagentDepthLimitError", () => { test("includes childDepth, maxDepth, and session IDs in message", () => { const error = createSubagentDepthLimitError({ @@ -239,18 +222,3 @@ describe("createSubagentDepthLimitError", () => { expect(error.message).toContain("spawn blocked") }) }) - -describe("createSubagentDescendantLimitError", () => { - test("includes descendant count, max, and root session ID", () => { - const error = createSubagentDescendantLimitError({ - rootSessionID: "root-789", - descendantCount: 50, - maxDescendants: 50, - }) - - expect(error.message).toContain("root-789") - expect(error.message).toContain("50") - expect(error.message).toContain("maxDescendants=50") - expect(error.message).toContain("spawn blocked") - }) -}) diff --git a/src/features/background-agent/subagent-spawn-limits.ts b/src/features/background-agent/subagent-spawn-limits.ts index c53a0e358..9483f3247 100644 --- a/src/features/background-agent/subagent-spawn-limits.ts +++ b/src/features/background-agent/subagent-spawn-limits.ts @@ -2,7 +2,6 @@ import type { BackgroundTaskConfig } from "../../config/schema" import type { OpencodeClient } from "./constants" export const DEFAULT_MAX_SUBAGENT_DEPTH = 3 -export const DEFAULT_MAX_ROOT_SESSION_SPAWN_BUDGET = 50 export interface SubagentSpawnContext { rootSessionID: string @@ -14,10 +13,6 @@ export function getMaxSubagentDepth(config?: BackgroundTaskConfig): number { return config?.maxDepth ?? DEFAULT_MAX_SUBAGENT_DEPTH } -export function getMaxRootSessionSpawnBudget(config?: BackgroundTaskConfig): number { - return config?.maxDescendants ?? DEFAULT_MAX_ROOT_SESSION_SPAWN_BUDGET -} - export async function resolveSubagentSpawnContext( client: OpencodeClient, parentSessionID: string, @@ -53,7 +48,7 @@ export async function resolveSubagentSpawnContext( } catch (error) { const reason = error instanceof Error ? error.message : String(error) throw new Error( - `Subagent spawn blocked: failed to resolve session lineage for ${parentSessionID}, so background_task.maxDescendants cannot be enforced safely. ${reason}` + `Subagent spawn blocked: failed to resolve session lineage for ${parentSessionID}, so background_task.maxDepth cannot be enforced safely. ${reason}` ) } @@ -84,14 +79,3 @@ export function createSubagentDepthLimitError(input: { `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.` - ) -} diff --git a/src/tools/delegate-task/sync-task.ts b/src/tools/delegate-task/sync-task.ts index 7675fa0de..4ec84696c 100644 --- a/src/tools/delegate-task/sync-task.ts +++ b/src/tools/delegate-task/sync-task.ts @@ -37,7 +37,7 @@ export async function executeSyncTask( spawnReservation = await manager.reserveSubagentSpawn(parentContext.sessionID) } - // Depth/descendant guard. We must NOT silently fall back to childDepth: 1 + // Depth guard. We must NOT silently fall back to childDepth: 1 // when the manager is unavailable or lacks the spawn methods, because that // would let subagents recurse without bound. The only safe fallback is // when the manager genuinely cannot enforce limits (legacy SDK), in which @@ -51,7 +51,7 @@ export async function executeSyncTask( } else { log( "[task] WARNING: BackgroundManager has no spawn enforcement methods (reserveSubagentSpawn / assertCanSpawn). " + - "Depth and descendant limits cannot be enforced for this task. This indicates an old SDK or a misconfiguration.", + "Depth limits cannot be enforced for this task. This indicates an old SDK or a misconfiguration.", { parentSessionID: parentContext.sessionID } ) spawnContext = {