From cc1c23032fc9a288c367eda8c0a3cf48071ee3ca Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 11 Mar 2026 21:45:39 +0900 Subject: [PATCH] fix(background-agent): handle SDK error response in spawn limit lineage lookup - Check response.error and !response.data after session.get() to fail closed - Prevents unlimited spawning when SDK returns non-throwing error responses - Added regression tests for SDK error and missing data scenarios --- .../subagent-spawn-limits.test.ts | 44 +++++++++++++++++++ .../background-agent/subagent-spawn-limits.ts | 12 ++++- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/features/background-agent/subagent-spawn-limits.test.ts diff --git a/src/features/background-agent/subagent-spawn-limits.test.ts b/src/features/background-agent/subagent-spawn-limits.test.ts new file mode 100644 index 000000000..154718dbd --- /dev/null +++ b/src/features/background-agent/subagent-spawn-limits.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test" +import type { OpencodeClient } from "./constants" +import { resolveSubagentSpawnContext } from "./subagent-spawn-limits" + +function createMockClient(sessionGet: OpencodeClient["session"]["get"]): OpencodeClient { + return { + session: { + get: sessionGet, + }, + } as OpencodeClient +} + +describe("resolveSubagentSpawnContext", () => { + describe("#given session.get returns an SDK error response", () => { + test("throws a fail-closed spawn blocked error", async () => { + // given + const client = createMockClient(async () => ({ + error: "lookup failed", + data: undefined, + })) + + // when + const result = resolveSubagentSpawnContext(client, "parent-session") + + // then + await expect(result).rejects.toThrow(/background_task\.maxDescendants cannot be enforced safely.*lookup failed/) + }) + }) + + describe("#given session.get returns no session data", () => { + test("throws a fail-closed spawn blocked error", async () => { + // given + const client = createMockClient(async () => ({ + data: undefined, + })) + + // when + const result = resolveSubagentSpawnContext(client, "parent-session") + + // then + await expect(result).rejects.toThrow(/background_task\.maxDescendants cannot be enforced safely.*No session data returned/) + }) + }) +}) diff --git a/src/features/background-agent/subagent-spawn-limits.ts b/src/features/background-agent/subagent-spawn-limits.ts index c33ad7b21..d8f3db4b8 100644 --- a/src/features/background-agent/subagent-spawn-limits.ts +++ b/src/features/background-agent/subagent-spawn-limits.ts @@ -36,10 +36,18 @@ export async function resolveSubagentSpawnContext( let nextParentSessionID: string | undefined try { - const session = await client.session.get({ + const response = await client.session.get({ path: { id: currentSessionID }, }) - nextParentSessionID = session.data?.parentID + if (response.error) { + throw new Error(String(response.error)) + } + + if (!response.data) { + throw new Error("No session data returned") + } + + nextParentSessionID = response.data.parentID } catch (error) { const reason = error instanceof Error ? error.message : String(error) throw new Error(