From 4a71579bf248ad0ebd748e3592c4920c0d43cfd5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 1 Nov 2026 00:55:52 +0000 Subject: [PATCH 1/2] test(server): reproduce #28448, nested subagent wake never reaches the parent When a delegated child's own reviewer subagent finishes, the delegated completion wake starts a new run in the child, but that run never becomes a follow-up task on the parent, so the parent is never told what it produced. Co-Authored-By: Claude Opus 5.5 --- .../DelegatedCompletionDelivery.test.ts | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts b/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts index 5053e8d568767..7696f73b141a1 100644 --- a/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts +++ b/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts @@ -1742,6 +1742,158 @@ it.layer(Layer.merge(layerTest, ProviderContinuationRequests.layer))( }), ); + it.effect( + "delivers the run a nested subagent's completion starts after the original result", + () => + Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const eventSink = yield* EventSink.EventSinkV2; + const requests = yield* ProviderContinuationRequests.ProviderContinuationRequests; + const now = yield* DateTime.now; + const parentThreadId = ThreadId.make("thread:nested-wake-parent"); + const projectId = ProjectId.make("project:nested-wake"); + const parentRunId = RunId.make("run:nested-wake-parent"); + const rootNodeId = NodeId.make("node:nested-wake-parent"); + yield* seedParentWithTerminalTask({ + threadId: parentThreadId, + projectId, + runId: parentRunId, + rootNodeId, + taskId: NodeId.make("node:nested-wake-seed"), + deliveryState: "acknowledged", + now, + }); + const child = yield* seedRestartCancelledChild({ + parentThreadId, + projectId, + parentRunId, + rootNodeId, + name: "nested-wake-child", + completionWake: "always", + continuationPending: false, + runStatus: "completed", + now, + }); + yield* orchestrator.recoverDelegatedTasks; + let firstWake = yield* requests.take; + while (firstWake.threadId !== parentThreadId) firstWake = yield* requests.take; + yield* orchestrator.dispatch({ + type: "delegated_task.completion-delivery.acknowledge", + commandId: CommandId.make("command:nested-wake-ack"), + parentThreadId, + taskId: child.taskId, + observedByRunId: parentRunId, + }); + const parentRun = (yield* orchestrator.getThreadProjection(parentThreadId)).runs.find( + (run) => run.id === parentRunId, + )!; + yield* eventSink.write({ + commandId: reconcileCommandId("nested-wake-parent-finished"), + events: [ + { + id: EventId.make("event:nested-wake-parent-finished"), + type: "run.updated", + threadId: parentThreadId, + runId: parentRunId, + occurredAt: now, + payload: { ...parentRun, status: "completed", completedAt: now }, + }, + ], + }); + + // The child delegated a reviewer of its own, which finishes after the child's turn ended. + yield* seedRestartCancelledChild({ + parentThreadId: child.childThreadId, + projectId, + parentRunId: child.childRunId, + rootNodeId: NodeId.make("node:nested-wake-child-root"), + name: "nested-wake-grandchild", + completionWake: "always", + continuationPending: false, + runStatus: "completed", + now, + }); + yield* orchestrator.recoverDelegatedTasks; + const delivery = (yield* orchestrator.getThreadProjection(child.childThreadId)).runs.find( + (run) => run.id === child.childRunId, + )!.delegatedCompletion!.delivery!; + + // The shape ProviderContinuationService dispatches for that delegated completion. + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "agent", + creationSource: "server", + commandId: CommandId.make("command:nested-wake"), + threadId: child.childThreadId, + messageId: delivery.messageId, + text: "Delegated task completed", + attachments: [], + delegatedCompletion: { + parentRunId: child.childRunId, + generation: delivery.generation, + taskIds: delivery.taskIds, + }, + dispatchMode: { type: "queue_after_active" }, + }); + const run = (yield* orchestrator.getThreadProjection(child.childThreadId)).runs.find( + (row) => row.userMessageId === delivery.messageId, + )!; + assert.isDefined( + run.delegatedTaskId, + "the nested completion's run is not a follow-up task", + ); + yield* eventSink.write({ + commandId: reconcileCommandId("nested-wake-finished"), + events: [ + { + id: EventId.make("event:nested-wake-result"), + type: "message.updated", + threadId: child.childThreadId, + runId: run.id, + occurredAt: now, + payload: { + id: MessageId.make("message:nested-wake-result"), + threadId: child.childThreadId, + runId: run.id, + nodeId: null, + role: "assistant", + text: "The reviewers approved the change.", + attachments: [], + streaming: false, + createdBy: "agent", + creationSource: "server", + createdAt: now, + updatedAt: now, + }, + }, + { + id: EventId.make("event:nested-wake-finished"), + type: "run.updated", + threadId: child.childThreadId, + runId: run.id, + occurredAt: now, + payload: { ...run, status: "completed", startedAt: now, completedAt: now }, + }, + ], + }); + yield* orchestrator.recoverDelegatedTasks; + const followup = (yield* orchestrator.getThreadProjection(parentThreadId)).subagents.find( + (task) => task.id === run.delegatedTaskId, + )!; + assert.equal(followup.status, "completed"); + assert.equal(followup.result, "The reviewers approved the change."); + assert.equal(followup.completionDelivery?.state, "claimed"); + let wake = yield* requests.take; + while ( + wake.threadId !== parentThreadId || + wake.delegatedCompletion?.messageId === firstWake.delegatedCompletion?.messageId + ) { + wake = yield* requests.take; + } + assert.equal(wake.delegatedCompletion?.parentRunId, parentRunId); + }), + ); + const outcomes = [ "completed", "failed", From 363dd0e681555f50be9070731b3c020d29d90142 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 1 Nov 2026 01:00:40 +0000 Subject: [PATCH 2/2] fix(server): parent hears when a nested subagent's completion wakes its child When a delegated child's own delegate finished after the child's task already had its result, the run that completion started never became a follow-up task on the parent, so the parent was never told when it ended. Count a delegated completion as a wake that may start a subagent follow-up, like background-command wakes, and let it past the follow-up sender check. Dispatch now also takes the parent's lock for these messages, since intake writes the parent. Fixes pingdotgg/t3code#28448 Co-Authored-By: Claude Opus 5.5 --- apps/server/src/orchestration-v2/Orchestrator.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 0a5e56d98d639..b2be7a9c4d585 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -634,7 +634,7 @@ function cancelledAppOwnedTask( }; } -// Provider wakes can start follow-up work; monitor updates only report progress. +// Provider wakes and delegated completions can start follow-ups; monitor updates only report progress. // Dispatch takes the parent's lock for follow-ups because intake writes the parent. function mayStartSubagentFollowup( command: Extract, @@ -642,6 +642,7 @@ function mayStartSubagentFollowup( if (command.scheduledTaskId !== undefined || command.restartContinuationOfRunId !== undefined) return false; if (command.notification !== undefined) return command.notification.source.kind !== "monitor"; + if (command.delegatedCompletion !== undefined) return true; return command.createdBy === "user" || command.creationSource === "mcp"; } @@ -7766,6 +7767,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio child.lineage.relationshipToParent !== "subagent" || parentThreadId === null || (command.notification === undefined && + command.delegatedCompletion === undefined && command.createdBy !== "user" && command.senderThreadId !== parentThreadId) )