From b1a638482d3f26a2e3c767f14d62cd0c791e715a Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:05:44 +0200 Subject: [PATCH] Cover child completion before create_agent returns (#6008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a regression test for #5928: a child that finishes its initial turn before its turn start returns still sends one finish notification to its parent. The fix itself is on main from 0009a7e. Co-authored-by: Ahmet İzgi --- .../src/server/agent/mcp-server.test.ts | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index f1311d31e2..de0099cecf 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -3296,6 +3296,50 @@ describe("create_agent MCP tool", () => { ); }); + it("notifies the caller when a created child finishes its initial turn before the turn start returns", async () => { + const workdir = await mkdtemp(join(tmpdir(), "mcp-create-fast-child-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const parentClient = new HeldTurnAgentClient("claude", false); + const childClient = new HeldTurnAgentClient("codex", false, { finishTurnsDuringStart: true }); + const agentManager = new AgentManager({ + clients: { claude: parentClient, codex: childClient }, + registry: storage, + logger, + }); + + try { + const parent = await agentManager.createAgent( + { provider: "claude", cwd: existingCwd }, + undefined, + { workspaceId: "wks_parent" }, + ); + const server = await createAgentMcpServer({ + agentManager, + agentStorage: storage, + callerAgentId: parent.id, + providerSnapshotManager: createOpenCodeManager().manager, + logger, + }); + + const response = await invokeToolWithParsedInput(registeredTool(server, "create_agent"), { + ...subagentCurrentWorkspace(), + title: "Fast Child", + provider: "codex/gpt-5.4", + initialPrompt: "Finish immediately", + }); + const childId = z.object({ agentId: z.string() }).parse(response.structuredContent).agentId; + + await vi.waitFor(() => { + const parentPrompts = parentClient.sessions[0]!.prompts; + expect(parentPrompts).toHaveLength(1); + expect(parentPrompts[0]).toContain(childId); + expect(parentPrompts[0]).toContain("finished"); + }); + } finally { + await removeAgentStateDir(agentManager, storage, workdir); + } + }); + it("creates detached caller agents without a parent label", async () => { const { agentManager, agentStorage, spies } = createTestDeps(); spies.agentManager.getAgent.mockReturnValue({ @@ -3770,6 +3814,11 @@ const HELD_TURN_CAPABILITIES = { supportsToolInvocations: false, } as const; +interface HeldTurnOptions { + /** Run the whole turn before the turn-start request returns. */ + finishTurnsDuringStart?: boolean; +} + /** * Provider session that records every prompt it receives. With `holdTurns`, a started * turn stays running until `finishTurn()` so a caller's bounded wait can run out first. @@ -3786,6 +3835,7 @@ class HeldTurnAgentSession implements AgentSession { constructor( readonly provider: AgentProvider, private readonly holdTurns: boolean, + private readonly options: HeldTurnOptions = {}, ) {} /** Hold the next turn's acknowledgment, as a provider awaiting its turn-start request does. */ @@ -3819,6 +3869,13 @@ class HeldTurnAgentSession implements AgentSession { throw error; } const turnId = randomUUID(); + if (this.options.finishTurnsDuringStart) { + // The whole turn runs before the turn-start request returns, as a provider whose + // start acknowledgment arrives together with the turn's completion does. + this.pushEvent({ type: "turn_started", provider: this.provider, turnId }); + this.pushEvent({ type: "turn_completed", provider: this.provider, turnId }); + return { turnId }; + } this.activeTurnId = turnId; this.pushEvent({ type: "timeline", @@ -3926,6 +3983,7 @@ class HeldTurnAgentClient implements AgentClient { constructor( readonly provider: AgentProvider, private readonly holdTurns: boolean, + private readonly options: HeldTurnOptions = {}, ) {} async isAvailable(): Promise { @@ -3933,7 +3991,7 @@ class HeldTurnAgentClient implements AgentClient { } async createSession(): Promise { - const session = new HeldTurnAgentSession(this.provider, this.holdTurns); + const session = new HeldTurnAgentSession(this.provider, this.holdTurns, this.options); this.sessions.push(session); return session; }