Count sync subagent spawns against descendant limits
This commit is contained in:
@@ -2036,6 +2036,28 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
|
|||||||
// then
|
// then
|
||||||
await expect(result).rejects.toThrow("background_task.maxDescendants=1")
|
await expect(result).rejects.toThrow("background_task.maxDescendants=1")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should consume descendant quota for reserved sync spawns", async () => {
|
||||||
|
// given
|
||||||
|
manager.shutdown()
|
||||||
|
manager = new BackgroundManager(
|
||||||
|
{
|
||||||
|
client: createMockClientWithSessionChain({
|
||||||
|
"session-root": { directory: "/test/dir" },
|
||||||
|
}),
|
||||||
|
directory: tmpdir(),
|
||||||
|
} as unknown as PluginInput,
|
||||||
|
{ maxDescendants: 1 },
|
||||||
|
)
|
||||||
|
|
||||||
|
await manager.reserveSubagentSpawn("session-root")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = manager.assertCanSpawn("session-root")
|
||||||
|
|
||||||
|
// then
|
||||||
|
await expect(result).rejects.toThrow("background_task.maxDescendants=1")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("pending task can be cancelled", () => {
|
describe("pending task can be cancelled", () => {
|
||||||
|
|||||||
@@ -178,12 +178,47 @@ export class BackgroundManager {
|
|||||||
return spawnContext
|
return spawnContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reserveSubagentSpawn(parentSessionID: string): Promise<{
|
||||||
|
spawnContext: SubagentSpawnContext
|
||||||
|
descendantCount: number
|
||||||
|
commit: () => number
|
||||||
|
rollback: () => void
|
||||||
|
}> {
|
||||||
|
const spawnContext = await this.assertCanSpawn(parentSessionID)
|
||||||
|
const descendantCount = this.registerRootDescendant(spawnContext.rootSessionID)
|
||||||
|
let settled = false
|
||||||
|
|
||||||
|
return {
|
||||||
|
spawnContext,
|
||||||
|
descendantCount,
|
||||||
|
commit: () => {
|
||||||
|
settled = true
|
||||||
|
return descendantCount
|
||||||
|
},
|
||||||
|
rollback: () => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
this.unregisterRootDescendant(spawnContext.rootSessionID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private registerRootDescendant(rootSessionID: string): number {
|
private registerRootDescendant(rootSessionID: string): number {
|
||||||
const nextCount = (this.rootDescendantCounts.get(rootSessionID) ?? 0) + 1
|
const nextCount = (this.rootDescendantCounts.get(rootSessionID) ?? 0) + 1
|
||||||
this.rootDescendantCounts.set(rootSessionID, nextCount)
|
this.rootDescendantCounts.set(rootSessionID, nextCount)
|
||||||
return nextCount
|
return nextCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unregisterRootDescendant(rootSessionID: string): void {
|
||||||
|
const currentCount = this.rootDescendantCounts.get(rootSessionID) ?? 0
|
||||||
|
if (currentCount <= 1) {
|
||||||
|
this.rootDescendantCounts.delete(rootSessionID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rootDescendantCounts.set(rootSessionID, currentCount - 1)
|
||||||
|
}
|
||||||
|
|
||||||
async launch(input: LaunchInput): Promise<BackgroundTask> {
|
async launch(input: LaunchInput): Promise<BackgroundTask> {
|
||||||
log("[background-agent] launch() called with:", {
|
log("[background-agent] launch() called with:", {
|
||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
@@ -196,14 +231,14 @@ export class BackgroundManager {
|
|||||||
throw new Error("Agent parameter is required")
|
throw new Error("Agent parameter is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
const spawnContext = await this.assertCanSpawn(input.parentSessionID)
|
const spawnReservation = await this.reserveSubagentSpawn(input.parentSessionID)
|
||||||
const descendantCount = this.registerRootDescendant(spawnContext.rootSessionID)
|
|
||||||
|
|
||||||
|
try {
|
||||||
log("[background-agent] spawn guard passed", {
|
log("[background-agent] spawn guard passed", {
|
||||||
parentSessionID: input.parentSessionID,
|
parentSessionID: input.parentSessionID,
|
||||||
rootSessionID: spawnContext.rootSessionID,
|
rootSessionID: spawnReservation.spawnContext.rootSessionID,
|
||||||
childDepth: spawnContext.childDepth,
|
childDepth: spawnReservation.spawnContext.childDepth,
|
||||||
descendantCount,
|
descendantCount: spawnReservation.descendantCount,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create task immediately with status="pending"
|
// Create task immediately with status="pending"
|
||||||
@@ -211,13 +246,13 @@ export class BackgroundManager {
|
|||||||
id: `bg_${crypto.randomUUID().slice(0, 8)}`,
|
id: `bg_${crypto.randomUUID().slice(0, 8)}`,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
queuedAt: new Date(),
|
queuedAt: new Date(),
|
||||||
rootSessionID: spawnContext.rootSessionID,
|
rootSessionID: spawnReservation.spawnContext.rootSessionID,
|
||||||
// Do NOT set startedAt - will be set when running
|
// Do NOT set startedAt - will be set when running
|
||||||
// Do NOT set sessionID - will be set when running
|
// Do NOT set sessionID - will be set when running
|
||||||
description: input.description,
|
description: input.description,
|
||||||
prompt: input.prompt,
|
prompt: input.prompt,
|
||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
spawnDepth: spawnContext.childDepth,
|
spawnDepth: spawnReservation.spawnContext.childDepth,
|
||||||
parentSessionID: input.parentSessionID,
|
parentSessionID: input.parentSessionID,
|
||||||
parentMessageID: input.parentMessageID,
|
parentMessageID: input.parentMessageID,
|
||||||
parentModel: input.parentModel,
|
parentModel: input.parentModel,
|
||||||
@@ -259,10 +294,16 @@ export class BackgroundManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spawnReservation.commit()
|
||||||
|
|
||||||
// Trigger processing (fire-and-forget)
|
// Trigger processing (fire-and-forget)
|
||||||
this.processKey(key)
|
this.processKey(key)
|
||||||
|
|
||||||
return { ...task }
|
return { ...task }
|
||||||
|
} catch (error) {
|
||||||
|
spawnReservation.rollback()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processKey(key: string): Promise<void> {
|
private async processKey(key: string): Promise<void> {
|
||||||
@@ -1476,14 +1517,6 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatDuration(start: Date, end?: Date): string {
|
|
||||||
return formatDuration(start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
private isAbortedSessionError(error: unknown): boolean {
|
|
||||||
return isAbortedSessionError(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasRunningTasks(): boolean {
|
private hasRunningTasks(): boolean {
|
||||||
for (const task of this.tasks.values()) {
|
for (const task of this.tasks.values()) {
|
||||||
if (task.status === "running") return true
|
if (task.status === "running") return true
|
||||||
|
|||||||
@@ -249,4 +249,52 @@ describe("executeSync", () => {
|
|||||||
expect(deps.waitForCompletion).not.toHaveBeenCalled()
|
expect(deps.waitForCompletion).not.toHaveBeenCalled()
|
||||||
expect(deps.processMessages).not.toHaveBeenCalled()
|
expect(deps.processMessages).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("commits reserved descendant quota after creating a new sync session", async () => {
|
||||||
|
//#given
|
||||||
|
const { executeSync } = require("./sync-executor")
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
createOrGetSession: mock(async () => ({ sessionID: "ses-test-789", isNew: true })),
|
||||||
|
waitForCompletion: mock(async () => {}),
|
||||||
|
processMessages: mock(async () => "agent response"),
|
||||||
|
setSessionFallbackChain: mock(() => {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const spawnReservation = {
|
||||||
|
commit: mock(() => 1),
|
||||||
|
rollback: mock(() => {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
subagent_type: "explore",
|
||||||
|
description: "test task",
|
||||||
|
prompt: "find something",
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolContext = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
messageID: "msg-4",
|
||||||
|
agent: "sisyphus",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
metadata: mock(async () => {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
client: {
|
||||||
|
session: {
|
||||||
|
promptAsync: mock(async () => ({ data: {} })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await executeSync(args, toolContext, ctx as any, deps, undefined, spawnReservation)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(spawnReservation.commit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(spawnReservation.rollback).toHaveBeenCalledTimes(0)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ type ExecuteSyncDeps = {
|
|||||||
setSessionFallbackChain: typeof setSessionFallbackChain
|
setSessionFallbackChain: typeof setSessionFallbackChain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SpawnReservation = {
|
||||||
|
commit: () => number
|
||||||
|
rollback: () => void
|
||||||
|
}
|
||||||
|
|
||||||
const defaultDeps: ExecuteSyncDeps = {
|
const defaultDeps: ExecuteSyncDeps = {
|
||||||
createOrGetSession,
|
createOrGetSession,
|
||||||
waitForCompletion,
|
waitForCompletion,
|
||||||
@@ -33,13 +38,22 @@ export async function executeSync(
|
|||||||
messageID: string
|
messageID: string
|
||||||
agent: string
|
agent: string
|
||||||
abort: AbortSignal
|
abort: AbortSignal
|
||||||
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void | Promise<void>
|
||||||
},
|
},
|
||||||
ctx: PluginInput,
|
ctx: PluginInput,
|
||||||
deps: ExecuteSyncDeps = defaultDeps,
|
deps: ExecuteSyncDeps = defaultDeps,
|
||||||
fallbackChain?: FallbackEntry[],
|
fallbackChain?: FallbackEntry[],
|
||||||
|
spawnReservation?: SpawnReservation,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const { sessionID } = await deps.createOrGetSession(args, toolContext, ctx)
|
let sessionID: string | undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await deps.createOrGetSession(args, toolContext, ctx)
|
||||||
|
sessionID = session.sessionID
|
||||||
|
|
||||||
|
if (session.isNew) {
|
||||||
|
spawnReservation?.commit()
|
||||||
|
}
|
||||||
|
|
||||||
if (fallbackChain && fallbackChain.length > 0) {
|
if (fallbackChain && fallbackChain.length > 0) {
|
||||||
deps.setSessionFallbackChain(sessionID, fallbackChain)
|
deps.setSessionFallbackChain(sessionID, fallbackChain)
|
||||||
@@ -83,4 +97,8 @@ export async function executeSync(
|
|||||||
responseText + "\n\n" + ["<task_metadata>", `session_id: ${sessionID}`, "</task_metadata>"].join("\n")
|
responseText + "\n\n" + ["<task_metadata>", `session_id: ${sessionID}`, "</task_metadata>"].join("\n")
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
} catch (error) {
|
||||||
|
spawnReservation?.rollback()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import { describe, test, expect, mock } from "bun:test"
|
const { beforeEach, describe, test, expect, mock } = require("bun:test")
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
const { createCallOmoAgent } = require("./tools")
|
||||||
import type { BackgroundManager } from "../../features/background-agent"
|
|
||||||
import type { FallbackEntry } from "../../shared/model-requirements"
|
|
||||||
import { createCallOmoAgent } from "./tools"
|
|
||||||
|
|
||||||
describe("createCallOmoAgent", () => {
|
describe("createCallOmoAgent", () => {
|
||||||
const assertCanSpawnMock = mock(() => Promise.resolve(undefined))
|
const assertCanSpawnMock = mock(() => Promise.resolve(undefined))
|
||||||
|
const reserveCommitMock = mock(() => 1)
|
||||||
|
const reserveRollbackMock = mock(() => {})
|
||||||
|
const reserveSubagentSpawnMock = mock(() => Promise.resolve({
|
||||||
|
spawnContext: { rootSessionID: "root-session", parentDepth: 0, childDepth: 1 },
|
||||||
|
descendantCount: 1,
|
||||||
|
commit: reserveCommitMock,
|
||||||
|
rollback: reserveRollbackMock,
|
||||||
|
}))
|
||||||
const mockCtx = {
|
const mockCtx = {
|
||||||
client: {},
|
client: {},
|
||||||
directory: "/test",
|
directory: "/test",
|
||||||
} as unknown as PluginInput
|
}
|
||||||
|
|
||||||
const mockBackgroundManager = {
|
const mockBackgroundManager = {
|
||||||
assertCanSpawn: assertCanSpawnMock,
|
assertCanSpawn: assertCanSpawnMock,
|
||||||
|
reserveSubagentSpawn: reserveSubagentSpawnMock,
|
||||||
launch: mock(() => Promise.resolve({
|
launch: mock(() => Promise.resolve({
|
||||||
id: "test-task-id",
|
id: "test-task-id",
|
||||||
sessionID: null,
|
sessionID: null,
|
||||||
@@ -20,7 +26,14 @@ describe("createCallOmoAgent", () => {
|
|||||||
agent: "test-agent",
|
agent: "test-agent",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
})),
|
})),
|
||||||
} as unknown as BackgroundManager
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
assertCanSpawnMock.mockClear()
|
||||||
|
reserveSubagentSpawnMock.mockClear()
|
||||||
|
reserveCommitMock.mockClear()
|
||||||
|
reserveRollbackMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
test("should reject agent in disabled_agents list", async () => {
|
test("should reject agent in disabled_agents list", async () => {
|
||||||
//#given
|
//#given
|
||||||
@@ -105,7 +118,7 @@ describe("createCallOmoAgent", () => {
|
|||||||
|
|
||||||
test("uses agent override fallback_models when launching background subagent", async () => {
|
test("uses agent override fallback_models when launching background subagent", async () => {
|
||||||
//#given
|
//#given
|
||||||
const launch = mock((_input: { fallbackChain?: FallbackEntry[] }) => Promise.resolve({
|
const launch = mock((_input: { fallbackChain?: Array<{ providers: string[]; model: string; variant?: string }> }) => Promise.resolve({
|
||||||
id: "task-fallback",
|
id: "task-fallback",
|
||||||
sessionID: "sub-session",
|
sessionID: "sub-session",
|
||||||
description: "Test task",
|
description: "Test task",
|
||||||
@@ -115,7 +128,7 @@ describe("createCallOmoAgent", () => {
|
|||||||
const managerWithLaunch = {
|
const managerWithLaunch = {
|
||||||
launch,
|
launch,
|
||||||
getTask: mock(() => undefined),
|
getTask: mock(() => undefined),
|
||||||
} as unknown as BackgroundManager
|
}
|
||||||
const toolDef = createCallOmoAgent(
|
const toolDef = createCallOmoAgent(
|
||||||
mockCtx,
|
mockCtx,
|
||||||
managerWithLaunch,
|
managerWithLaunch,
|
||||||
@@ -154,7 +167,7 @@ describe("createCallOmoAgent", () => {
|
|||||||
|
|
||||||
test("should return a tool error when sync spawn depth validation fails", async () => {
|
test("should return a tool error when sync spawn depth validation fails", async () => {
|
||||||
//#given
|
//#given
|
||||||
assertCanSpawnMock.mockRejectedValueOnce(new Error("Subagent spawn blocked: child depth 4 exceeds background_task.maxDepth=3."))
|
reserveSubagentSpawnMock.mockRejectedValueOnce(new Error("Subagent spawn blocked: child depth 4 exceeds background_task.maxDepth=3."))
|
||||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||||
const executeFunc = toolDef.execute as Function
|
const executeFunc = toolDef.execute as Function
|
||||||
|
|
||||||
@@ -173,3 +186,5 @@ describe("createCallOmoAgent", () => {
|
|||||||
expect(result).toContain("background_task.maxDepth=3")
|
expect(result).toContain("background_task.maxDepth=3")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|||||||
@@ -96,9 +96,12 @@ export function createCallOmoAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!args.session_id) {
|
if (!args.session_id) {
|
||||||
|
let spawnReservation: Awaited<ReturnType<BackgroundManager["reserveSubagentSpawn"]>> | undefined
|
||||||
try {
|
try {
|
||||||
await backgroundManager.assertCanSpawn(toolCtx.sessionID)
|
spawnReservation = await backgroundManager.reserveSubagentSpawn(toolCtx.sessionID)
|
||||||
|
return await executeSync(args, toolCtx, ctx, undefined, fallbackChain, spawnReservation)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
spawnReservation?.rollback()
|
||||||
return `Error: ${error instanceof Error ? error.message : String(error)}`
|
return `Error: ${error instanceof Error ? error.message : String(error)}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,65 @@ describe("executeSyncTask - cleanup on error paths", () => {
|
|||||||
expect(deleteCalls[0]).toBe("ses_test_12345678")
|
expect(deleteCalls[0]).toBe("ses_test_12345678")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("rolls back reserved descendant quota when sync session creation fails", async () => {
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
create: async () => ({ data: { id: "ses_test_12345678" } }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncTask } = require("./sync-task")
|
||||||
|
|
||||||
|
const commit = mock(() => 1)
|
||||||
|
const rollback = mock(() => {})
|
||||||
|
const reserveSubagentSpawn = mock(async () => ({
|
||||||
|
spawnContext: { rootSessionID: "parent-session", parentDepth: 0, childDepth: 1 },
|
||||||
|
descendantCount: 1,
|
||||||
|
commit,
|
||||||
|
rollback,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
createSyncSession: async () => ({ ok: false as const, error: "Failed to create session" }),
|
||||||
|
sendSyncPrompt: async () => null,
|
||||||
|
pollSyncSession: async () => null,
|
||||||
|
fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
manager: { reserveSubagentSpawn },
|
||||||
|
client: mockClient,
|
||||||
|
directory: "/tmp",
|
||||||
|
onSyncSessionCreated: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
prompt: "test prompt",
|
||||||
|
description: "test task",
|
||||||
|
category: "test",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
command: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
}, "test-agent", undefined, undefined, undefined, undefined, deps)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("Failed to create session")
|
||||||
|
expect(reserveSubagentSpawn).toHaveBeenCalledWith("parent-session")
|
||||||
|
expect(commit).toHaveBeenCalledTimes(0)
|
||||||
|
expect(rollback).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
test("cleans up toast and subagentSessions when pollSyncSession returns error", async () => {
|
test("cleans up toast and subagentSessions when pollSyncSession returns error", async () => {
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
session: {
|
session: {
|
||||||
@@ -182,7 +241,18 @@ describe("executeSyncTask - cleanup on error paths", () => {
|
|||||||
metadata: () => {},
|
metadata: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commit = mock(() => 1)
|
||||||
|
const rollback = mock(() => {})
|
||||||
|
|
||||||
const mockExecutorCtx = {
|
const mockExecutorCtx = {
|
||||||
|
manager: {
|
||||||
|
reserveSubagentSpawn: mock(async () => ({
|
||||||
|
spawnContext: { rootSessionID: "parent-session", parentDepth: 0, childDepth: 1 },
|
||||||
|
descendantCount: 1,
|
||||||
|
commit,
|
||||||
|
rollback,
|
||||||
|
})),
|
||||||
|
},
|
||||||
client: mockClient,
|
client: mockClient,
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
onSyncSessionCreated: null,
|
onSyncSessionCreated: null,
|
||||||
@@ -204,9 +274,14 @@ describe("executeSyncTask - cleanup on error paths", () => {
|
|||||||
|
|
||||||
//#then - should complete and cleanup resources
|
//#then - should complete and cleanup resources
|
||||||
expect(result).toContain("Task completed")
|
expect(result).toContain("Task completed")
|
||||||
|
expect(mockExecutorCtx.manager.reserveSubagentSpawn).toHaveBeenCalledWith("parent-session")
|
||||||
|
expect(commit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(rollback).toHaveBeenCalledTimes(0)
|
||||||
expect(removeTaskCalls.length).toBe(1)
|
expect(removeTaskCalls.length).toBe(1)
|
||||||
expect(removeTaskCalls[0]).toBe("sync_ses_test")
|
expect(removeTaskCalls[0]).toBe("sync_ses_test")
|
||||||
expect(deleteCalls.length).toBe(1)
|
expect(deleteCalls.length).toBe(1)
|
||||||
expect(deleteCalls[0]).toBe("ses_test_12345678")
|
expect(deleteCalls[0]).toBe("ses_test_12345678")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|||||||
@@ -27,15 +27,23 @@ export async function executeSyncTask(
|
|||||||
const toastManager = getTaskToastManager()
|
const toastManager = getTaskToastManager()
|
||||||
let taskId: string | undefined
|
let taskId: string | undefined
|
||||||
let syncSessionID: string | undefined
|
let syncSessionID: string | undefined
|
||||||
|
let spawnReservation:
|
||||||
|
| Awaited<ReturnType<ExecutorContext["manager"]["reserveSubagentSpawn"]>>
|
||||||
|
| undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const spawnContext = typeof manager?.assertCanSpawn === "function"
|
if (typeof manager?.reserveSubagentSpawn === "function") {
|
||||||
|
spawnReservation = await manager.reserveSubagentSpawn(parentContext.sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
const spawnContext = spawnReservation?.spawnContext
|
||||||
|
?? (typeof manager?.assertCanSpawn === "function"
|
||||||
? await manager.assertCanSpawn(parentContext.sessionID)
|
? await manager.assertCanSpawn(parentContext.sessionID)
|
||||||
: {
|
: {
|
||||||
rootSessionID: parentContext.sessionID,
|
rootSessionID: parentContext.sessionID,
|
||||||
parentDepth: 0,
|
parentDepth: 0,
|
||||||
childDepth: 1,
|
childDepth: 1,
|
||||||
}
|
})
|
||||||
|
|
||||||
const createSessionResult = await deps.createSyncSession(client, {
|
const createSessionResult = await deps.createSyncSession(client, {
|
||||||
parentSessionID: parentContext.sessionID,
|
parentSessionID: parentContext.sessionID,
|
||||||
@@ -45,10 +53,12 @@ export async function executeSyncTask(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!createSessionResult.ok) {
|
if (!createSessionResult.ok) {
|
||||||
|
spawnReservation?.rollback()
|
||||||
return createSessionResult.error
|
return createSessionResult.error
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionID = createSessionResult.sessionID
|
const sessionID = createSessionResult.sessionID
|
||||||
|
spawnReservation?.commit()
|
||||||
syncSessionID = sessionID
|
syncSessionID = sessionID
|
||||||
subagentSessions.add(sessionID)
|
subagentSessions.add(sessionID)
|
||||||
syncSubagentSessions.add(sessionID)
|
syncSubagentSessions.add(sessionID)
|
||||||
@@ -156,6 +166,7 @@ session_id: ${sessionID}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
spawnReservation?.rollback()
|
||||||
return formatDetailedError(error, {
|
return formatDetailedError(error, {
|
||||||
operation: "Execute task",
|
operation: "Execute task",
|
||||||
args,
|
args,
|
||||||
|
|||||||
Reference in New Issue
Block a user