@@ -59,6 +59,7 @@ import {
5959 sendLastMessageOnly ,
6060 type CurrentTurn ,
6161 type ParkedApproval ,
62+ type ParkedApprovedExecution ,
6263 type RunTurnOptions ,
6364 type SessionEnvironment ,
6465} from "./runtime-contracts.ts" ;
@@ -105,6 +106,11 @@ export async function runTurn(
105106 // Reset the per-turn approval-park bookkeeping. A fresh turn starts with no parked gates; this
106107 // turn re-records them only if it pauses on ACP permission gates. (The dispatch has already
107108 // captured any prior park into `opts.resume` before calling us.)
109+ const carriedApprovedExecutions = opts . resume
110+ ? [ ...( env . parkedApprovedExecutions ?. values ( ) ?? [ ] ) ]
111+ : [ ] ;
112+ const parkedApprovedExecutions = new Map < string , ParkedApprovedExecution > ( ) ;
113+ env . parkedApprovedExecutions = parkedApprovedExecutions ;
108114 env . parkedApprovals . clear ( ) ;
109115 env . parkedApproval = undefined ;
110116 env . approvalGateCount = 0 ;
@@ -213,6 +219,9 @@ export async function runTurn(
213219 void pause . signal . then ( ( ) => runLimits . notePaused ( ) ) ;
214220
215221 const openToolCallIds = ( ) : string [ ] => run . openToolCallIds ?.( ) ?? [ ] ;
222+ const approvedExecutionSeeds = new Map < string , ParkedApprovedExecution > (
223+ carriedApprovedExecutions . map ( ( seed ) => [ seed . toolCallId , seed ] ) ,
224+ ) ;
216225 const bufferedPausedCompletedFrames = new Map < string , unknown > ( ) ;
217226 const toolCallClosureWaiters = new Map < string , Set < ( ) => void > > ( ) ;
218227 const notifyToolCallClosed = ( toolCallId : string ) : void => {
@@ -288,6 +297,30 @@ export async function runTurn(
288297 ) {
289298 env . lastTurnToolCallIds . push ( frame . toolCallId ) ;
290299 }
300+ if (
301+ frame ?. sessionUpdate === "tool_call" &&
302+ typeof frame . toolCallId === "string" &&
303+ frame . toolCallId
304+ ) {
305+ const announced = update as {
306+ name ?: unknown ;
307+ title ?: unknown ;
308+ kind ?: unknown ;
309+ rawInput ?: unknown ;
310+ input ?: unknown ;
311+ } ;
312+ const existing = approvedExecutionSeeds . get ( frame . toolCallId ) ;
313+ const toolName = [ announced . name , announced . title , announced . kind ]
314+ . find (
315+ ( value ) : value is string =>
316+ typeof value === "string" && value . length > 0 ,
317+ ) ?? existing ?. toolName ;
318+ approvedExecutionSeeds . set ( frame . toolCallId , {
319+ toolCallId : frame . toolCallId ,
320+ toolName,
321+ args : announced . rawInput ?? announced . input ?? existing ?. args ,
322+ } ) ;
323+ }
291324 const toolCallId =
292325 typeof rawFrame . toolCallId === "string"
293326 ? rawFrame . toolCallId
@@ -339,6 +372,18 @@ export async function runTurn(
339372 extractClientToolOutputs ( request ) ,
340373 ) ;
341374 const executionGrants = new ApprovedExecutionGrants ( ) ;
375+ const seedApprovedExecution = ( seed : ParkedApprovedExecution ) : void => {
376+ approvedExecutionSeeds . set ( seed . toolCallId , seed ) ;
377+ run . handleUpdate ( {
378+ sessionUpdate : "tool_call" ,
379+ toolCallId : seed . toolCallId ,
380+ title : seed . toolName ,
381+ kind : seed . toolName ,
382+ rawInput : seed . args ,
383+ } ) ;
384+ pause . markAllowedExecution ( seed . toolCallId ) ;
385+ executionGrants . grant ( seed . toolName , seed . args ) ;
386+ } ;
342387 const responder =
343388 deps . responderFactory ?.( request ) ??
344389 new ApprovalResponder ( permissionPlan , decisions , logger ) ;
@@ -606,6 +651,9 @@ export async function runTurn(
606651 const decisions = opts . resume . decisions ;
607652 promptPromise = Promise . resolve ( decisions [ 0 ] ?. promptPromise ) ;
608653 promptPromise . catch ( ( ) => { } ) ;
654+ for ( const seed of carriedApprovedExecutions ) {
655+ seedApprovedExecution ( seed ) ;
656+ }
609657 for ( const decision of decisions ) {
610658 // Seed this run's trace with the parked tool call so the completing `tool_call_update`
611659 // closes it and the FE approval part flips to output-available even if the adapter
@@ -622,6 +670,11 @@ export async function runTurn(
622670 // confirm resolves) passes the relay guard. Claude resumes grant too — harmlessly, no
623671 // guard consults it.
624672 if ( decision . reply === "once" ) {
673+ approvedExecutionSeeds . set ( decision . toolCallId , {
674+ toolCallId : decision . toolCallId ,
675+ toolName : decision . toolName ,
676+ args : decision . args ,
677+ } ) ;
625678 pause . markAllowedExecution ( decision . toolCallId ) ;
626679 executionGrants . grant ( decision . toolName , decision . args ) ;
627680 }
@@ -674,19 +727,42 @@ export async function runTurn(
674727 settleBufferedPausedCompletions ( ) ;
675728 const openAllowedExecutions = openToolCallIds ( )
676729 . filter ( ( id ) => pause . isAllowedExecution ( id ) ) ;
677- await Promise . all (
678- openAllowedExecutions . map ( async ( toolCallId ) => {
679- const closed = await waitForToolCallClosure (
730+ const piBatchBlockedByApproval = Boolean (
731+ opts . resume &&
732+ plan . isPi &&
733+ opts . approvalParkMode &&
734+ env . parkedApprovals . size > 0 ,
735+ ) ;
736+ if ( piBatchBlockedByApproval ) {
737+ // Pi prepares every call in a parallel batch before it executes any of them. While a
738+ // sibling gate is pending, closure is impossible, so carry the approved spans and park.
739+ for ( const toolCallId of openAllowedExecutions ) {
740+ const seed = approvedExecutionSeeds . get ( toolCallId ) ?? {
680741 toolCallId,
681- resolvedRunLimits . toolCallMs ,
682- ) ;
683- if ( closed ) return ;
742+ toolName : undefined ,
743+ args : undefined ,
744+ } ;
745+ parkedApprovedExecutions . set ( toolCallId , seed ) ;
684746 run . settleOpenToolCalls (
685747 ( id ) => id !== toolCallId ,
686748 APPROVED_EXECUTION_RESULT_UNKNOWN ,
687749 ) ;
688- } ) ,
689- ) ;
750+ }
751+ } else {
752+ await Promise . all (
753+ openAllowedExecutions . map ( async ( toolCallId ) => {
754+ const closed = await waitForToolCallClosure (
755+ toolCallId ,
756+ resolvedRunLimits . toolCallMs ,
757+ ) ;
758+ if ( closed ) return ;
759+ run . settleOpenToolCalls (
760+ ( id ) => id !== toolCallId ,
761+ APPROVED_EXECUTION_RESULT_UNKNOWN ,
762+ ) ;
763+ } ) ,
764+ ) ;
765+ }
690766 settleBufferedPausedCompletions ( ) ;
691767 run . settleOpenToolCalls (
692768 ( id ) =>
0 commit comments