@@ -95,8 +95,7 @@ import {
9595 SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS ,
9696 SUBAGENT_WAIT_TOOL_DESCRIPTION ,
9797} from "./src/prompt.ts" ;
98- import { createDeferredResultDelivery } from "./src/result-delivery.ts" ;
99- import { resultDeliveryOptions } from "../background-terminals/src/result-delivery.ts" ;
98+ import { createSubagentResultDelivery } from "./src/result-delivery.ts" ;
10099import {
101100 effectiveChildToolAllowlist ,
102101 resolveStandaloneChildProjectTrust ,
@@ -210,7 +209,7 @@ export function createSubagentResultDispatcher(
210209 pi : ExtensionAPI ,
211210 outputFor : ( snap : SubagentSnapshot ) => string = truncatedOutput ,
212211) {
213- return ( snaps : readonly SubagentSnapshot [ ] , wake : boolean ) => {
212+ return ( snaps : readonly SubagentSnapshot [ ] , wake = true ) => {
214213 if ( snaps . length === 0 ) return ;
215214 const content = snaps
216215 . map ( ( snap ) =>
@@ -249,11 +248,37 @@ export function createSubagentResultDispatcher(
249248 display : false ,
250249 details,
251250 } ,
252- resultDeliveryOptions ( wake ) ,
251+ wake
252+ ? { deliverAs : "followUp" , triggerTurn : true }
253+ : { deliverAs : "nextTurn" } ,
253254 ) ;
254255 } ;
255256}
256257
258+ export function registerSubagentResultDeliveryLifecycle (
259+ pi : ExtensionAPI ,
260+ delivery : { parentSettled ( aborted ?: boolean ) : void } ,
261+ ) {
262+ let parentRunAborted = false ;
263+
264+ pi . on ( "agent_start" , ( ) => {
265+ parentRunAborted = false ;
266+ } ) ;
267+ pi . on ( "agent_end" , ( event ) => {
268+ for ( let index = event . messages . length - 1 ; index >= 0 ; index -- ) {
269+ const message = event . messages [ index ] ;
270+ if ( message ?. role !== "assistant" ) continue ;
271+ parentRunAborted = message . stopReason === "aborted" ;
272+ break ;
273+ }
274+ } ) ;
275+ pi . on ( "agent_settled" , ( ) => delivery . parentSettled ( parentRunAborted ) ) ;
276+
277+ return ( ) => {
278+ parentRunAborted = false ;
279+ } ;
280+ }
281+
257282type SubagentResultTheme = Parameters < MessageRenderer > [ 2 ] ;
258283
259284function renderSubagentResult (
@@ -322,8 +347,17 @@ export default function (pi: ExtensionAPI) {
322347 let requestWidgetRender : ( ( ) => void ) | undefined ;
323348 let navigationLayerRegistered = false ;
324349 let dashboardOpen = false ;
325- const resultDelivery = createDeferredResultDelivery < SubagentSnapshot > ( ) ;
326350 const dispatchResults = createSubagentResultDispatcher ( pi ) ;
351+ const resultDelivery = createSubagentResultDelivery < SubagentSnapshot > ( {
352+ isIdle : ( ) => sessionContext ?. isIdle ( ) === true ,
353+ // Every unconsumed fire-and-forget result must reach the parent. The
354+ // delivery coordinator batches results that settled while it was busy.
355+ deliver : dispatchResults ,
356+ } ) ;
357+ const resetResultDeliveryLifecycle = registerSubagentResultDeliveryLifecycle (
358+ pi ,
359+ resultDelivery ,
360+ ) ;
327361 const hideLifecycleTools = ( ) =>
328362 patchOwnedTools ( pi , "subagents" , {
329363 disable : OPENPI_TOOL_SURFACE . subagents . deferred ,
@@ -436,25 +470,6 @@ export default function (pi: ExtensionAPI) {
436470 navigationLayerRegistered = true ;
437471 } ;
438472
439- /**
440- * `wake` decides whether this costs the model a turn. A subagent that
441- * settled while the model sits idle is the result it is waiting on. A
442- * backlog that piled up while it worked is not: waking once per stale
443- * subagent forces a turn each, and the model can only answer "that one
444- * already finished". `nextTurn` still enters context with the user's next
445- * message, without demanding a reply.
446- */
447- const deliverResults = (
448- snaps : readonly SubagentSnapshot [ ] ,
449- wake : boolean ,
450- ) => {
451- dispatchResults ( snaps , wake ) ;
452- } ;
453-
454- const flushResults = ( wake : boolean ) => {
455- deliverResults ( resultDelivery . drain ( ) , wake ) ;
456- } ;
457-
458473 const deliverBtwResult = ( snap : SubagentSnapshot ) => {
459474 // appendEntry is a synchronous SessionManager operation and emits an
460475 // entry_appended event, so it is safe while the parent is streaming and
@@ -500,10 +515,10 @@ export default function (pi: ExtensionAPI) {
500515 // subagent_wait can consume it before agent_settled flushes follow-ups.
501516 // Defer a copy: the live snapshot keeps mutating if the subagent is
502517 // restarted before the deferred result flushes.
518+ // The delivery coordinator closes both sides of the wake-up race: it
519+ // flushes now if the parent is already idle, otherwise the parent's next
520+ // agent_settled edge rechecks this same pending Map.
503521 resultDelivery . defer ( { ...snap , meta : { ...snap . meta } } ) ;
504- // Settled while the model sits idle: it has nothing else in flight, so
505- // this is the result it is waiting on — wake it.
506- if ( sessionContext ?. isIdle ( ) ) flushResults ( true ) ;
507522 } ;
508523
509524 pi . on ( "session_start" , ( _event , ctx ) => {
@@ -530,10 +545,6 @@ export default function (pi: ExtensionAPI) {
530545 managerPromise ?. then ( updateStatus ) . catch ( ( ) => undefined ) ;
531546 } ) ;
532547
533- // These settled while the model was working on something else, so they go
534- // into context without forcing a turn per stale subagent.
535- pi . on ( "agent_settled" , ( ) => flushResults ( false ) ) ;
536-
537548 pi . on ( "session_shutdown" , async ( ) => {
538549 if ( navigationLayerRegistered ) {
539550 removeEditorLayer ( pi , "subagents" ) ;
@@ -555,6 +566,7 @@ export default function (pi: ExtensionAPI) {
555566 requestWidgetRender = undefined ;
556567 stripState . focused = false ;
557568 dashboardOpen = false ;
569+ resetResultDeliveryLifecycle ( ) ;
558570 const closing = runtime ;
559571 runtime = undefined ;
560572 managerPromise = undefined ;
0 commit comments