@@ -494,144 +494,147 @@ export function createTaskTool(deps: TaskToolDeps): AgentTool {
494494 let worktreeCwd : string | undefined ;
495495 let worktreeStashBaseline : readonly string [ ] | null = [ ] ;
496496 let worktreeHeadAtCreate : string | undefined ;
497- if ( deps . useWorktree === true ) {
498- const worktreePath = join ( deps . getWorkdirBase ( ) , "worktrees" , generateSessionId ( ) ) ;
499- try {
500- const worktree = await createSubAgentWorktree ( deps . cwd , worktreePath ) ;
501- worktreeCwd = worktree . path ;
502- worktreeStashBaseline = worktree . stashBaseline ;
503- worktreeHeadAtCreate = worktree . headAtCreate ;
504- } catch ( err ) {
505- // Admit already happened and the strip session may be "running" —
506- // release the ledger slot and fail the session so a worktree setup
507- // error never burns turn-budget budget or leaves a ghost row.
508- const message =
509- err instanceof WorktreeError
510- ? err . message
511- : `sub-agent worktree setup failed: ${ err instanceof Error ? err . message : String ( err ) } ` ;
512- briefLedger . release ( fingerprint ) ;
513- if ( session !== undefined ) deps . sessions ?. fail ( session . id , message ) ;
514- signal . removeEventListener ( "abort" , onParentAbort ) ;
515- return taskToolResult ( call . id , `Error: ${ message } ` ) ;
516- }
517- }
518- // Cleanup runs once the sub-agent's report is ready, regardless of
519- // outcome, so a cancelled or failed run's worktree is still reclaimed
520- // (or preserved with a notice) rather than leaked.
521- const finishWithWorktree = async ( result : ToolResult ) : Promise < ToolResult > => {
522- if ( worktreeCwd === undefined ) return result ;
523- const cleanup = await cleanupSubAgentWorktree ( deps . cwd , worktreeCwd , {
524- stashBaseline : worktreeStashBaseline ,
525- ...( worktreeHeadAtCreate !== undefined ? { headAtCreate : worktreeHeadAtCreate } : { } ) ,
526- } ) ;
527- if ( cleanup . status === "preserved" ) {
528- return { ...result , content : `${ result . content } \n\n${ cleanup . notice } ` } ;
529- }
530- return result ;
531- } ;
532-
497+ // Full child wall: worktree setup → run → teardown (CL-5170 exclusive share).
498+ const turnId = currentTurnId ( ) ;
499+ const subagentSpanId = start ( "subagent" , {
500+ ...( turnId !== null && turnId . length > 0 ? { parentId : turnId } : { } ) ,
501+ tags : {
502+ subagent_id : call . id ,
503+ ...( turnId !== null && turnId . length > 0 ? { turn_id : turnId } : { } ) ,
504+ } ,
505+ } ) ;
533506 try {
534- const params : RunSubAgentParams = {
535- ...sandbox ,
536- cwd : worktreeCwd ?? deps . cwd ,
537- workdirBase : deps . getWorkdirBase ( ) ,
538- provider,
539- ...( tier !== undefined ? { tier } : { } ) ,
540- ...( settings !== undefined ? { settings } : { } ) ,
541- ...( catalog !== undefined ? { catalog } : { } ) ,
542- description,
543- ...( context !== undefined && context . length > 0 ? { context } : { } ) ,
544- prompt,
545- ...( goals . length > 0 ? { goals } : { } ) ,
546- ...( intent !== undefined ? { intent } : { } ) ,
547- ...( successCriteria . length > 0 ? { successCriteria } : { } ) ,
548- ...( doNot . length > 0 ? { doNot } : { } ) ,
549- ...( reportFocus !== undefined && reportFocus . length > 0 ? { reportFocus } : { } ) ,
550- signal : childCtl . signal ,
551- ...( recordEvent !== undefined ? { onEvent : recordEvent } : { } ) ,
552- ...( deps . onProgress !== undefined ? { onProgress : deps . onProgress } : { } ) ,
553- ...( capabilities !== undefined ? { capabilities } : { } ) ,
554- ...( systemPromptRole !== undefined ? { systemPromptRole } : { } ) ,
555- ...( orchestrator
556- ? { orchestrator : true , nestedDispatch : nestedDispatch ! }
557- : { } ) ,
558- // Nested workers (installed by an orchestrator that already holds a
559- // slot) reuse the parent slot rather than acquiring their own.
560- ...( deps . allowOrchestrator === false ? { nested : true } : { } ) ,
561- maxTurns : resolvedMaxTurns ,
562- ...( deps . deadlineMs !== undefined ? { deadlineMs : deps . deadlineMs } : { } ) ,
563- } ;
564- const turnId = currentTurnId ( ) ;
565- const subagentSpanId = start ( "subagent" , {
566- ...( turnId !== null && turnId . length > 0 ? { parentId : turnId } : { } ) ,
567- tags : {
568- subagent_id : call . id ,
569- ...( turnId !== null && turnId . length > 0 ? { turn_id : turnId } : { } ) ,
570- } ,
571- } ) ;
572- const result = await run ( params ) . finally ( ( ) => {
573- end ( subagentSpanId ) ;
574- } ) ;
575- // Operator cancel may race after run resolves. Keep strip status cancelled
576- // when requested, but never discard a returned body (including salvage).
577- const wasCancelled =
578- childCtl . signal . aborted ||
579- ( session !== undefined &&
580- deps . sessions ?. get ( session . id ) ?. status === "cancelled" ) ;
581- const salvage = classifyBriefSalvage ( result ) ;
582- briefLedger . recordOutcome ( fingerprint , salvage ) ;
583- const hintOptions = {
584- dispatchCount,
585- turnBudgetStopAfterDispatches : TURN_BUDGET_STOP_AFTER_DISPATCHES ,
507+ if ( deps . useWorktree === true ) {
508+ const worktreePath = join ( deps . getWorkdirBase ( ) , "worktrees" , generateSessionId ( ) ) ;
509+ try {
510+ const worktree = await createSubAgentWorktree ( deps . cwd , worktreePath ) ;
511+ worktreeCwd = worktree . path ;
512+ worktreeStashBaseline = worktree . stashBaseline ;
513+ worktreeHeadAtCreate = worktree . headAtCreate ;
514+ } catch ( err ) {
515+ // Admit already happened and the strip session may be "running" —
516+ // release the ledger slot and fail the session so a worktree setup
517+ // error never burns turn-budget budget or leaves a ghost row.
518+ const message =
519+ err instanceof WorktreeError
520+ ? err . message
521+ : `sub-agent worktree setup failed: ${ err instanceof Error ? err . message : String ( err ) } ` ;
522+ briefLedger . release ( fingerprint ) ;
523+ if ( session !== undefined ) deps . sessions ?. fail ( session . id , message ) ;
524+ signal . removeEventListener ( "abort" , onParentAbort ) ;
525+ return taskToolResult ( call . id , `Error: ${ message } ` ) ;
526+ }
527+ }
528+ // Cleanup runs once the sub-agent's report is ready, regardless of
529+ // outcome, so a cancelled or failed run's worktree is still reclaimed
530+ // (or preserved with a notice) rather than leaked.
531+ const finishWithWorktree = async ( result : ToolResult ) : Promise < ToolResult > => {
532+ if ( worktreeCwd === undefined ) return result ;
533+ const cleanup = await cleanupSubAgentWorktree ( deps . cwd , worktreeCwd , {
534+ stashBaseline : worktreeStashBaseline ,
535+ ...( worktreeHeadAtCreate !== undefined ? { headAtCreate : worktreeHeadAtCreate } : { } ) ,
536+ } ) ;
537+ if ( cleanup . status === "preserved" ) {
538+ return { ...result , content : `${ result . content } \n\n${ cleanup . notice } ` } ;
539+ }
540+ return result ;
586541 } ;
587- if ( wasCancelled ) {
588- if (
589- session !== undefined &&
590- deps . sessions ?. get ( session . id ) ?. status === "running"
591- ) {
592- deps . sessions . cancel ( session . id , cancelReason ( childCtl . signal ) ) ;
542+
543+ try {
544+ const params : RunSubAgentParams = {
545+ ...sandbox ,
546+ cwd : worktreeCwd ?? deps . cwd ,
547+ workdirBase : deps . getWorkdirBase ( ) ,
548+ provider,
549+ ...( tier !== undefined ? { tier } : { } ) ,
550+ ...( settings !== undefined ? { settings } : { } ) ,
551+ ...( catalog !== undefined ? { catalog } : { } ) ,
552+ description,
553+ ...( context !== undefined && context . length > 0 ? { context } : { } ) ,
554+ prompt,
555+ ...( goals . length > 0 ? { goals } : { } ) ,
556+ ...( intent !== undefined ? { intent } : { } ) ,
557+ ...( successCriteria . length > 0 ? { successCriteria } : { } ) ,
558+ ...( doNot . length > 0 ? { doNot } : { } ) ,
559+ ...( reportFocus !== undefined && reportFocus . length > 0 ? { reportFocus } : { } ) ,
560+ signal : childCtl . signal ,
561+ ...( recordEvent !== undefined ? { onEvent : recordEvent } : { } ) ,
562+ ...( deps . onProgress !== undefined ? { onProgress : deps . onProgress } : { } ) ,
563+ ...( capabilities !== undefined ? { capabilities } : { } ) ,
564+ ...( systemPromptRole !== undefined ? { systemPromptRole } : { } ) ,
565+ ...( orchestrator
566+ ? { orchestrator : true , nestedDispatch : nestedDispatch ! }
567+ : { } ) ,
568+ // Nested workers (installed by an orchestrator that already holds a
569+ // slot) reuse the parent slot rather than acquiring their own.
570+ ...( deps . allowOrchestrator === false ? { nested : true } : { } ) ,
571+ maxTurns : resolvedMaxTurns ,
572+ ...( deps . deadlineMs !== undefined ? { deadlineMs : deps . deadlineMs } : { } ) ,
573+ } ;
574+ const result = await run ( params ) ;
575+ // Operator cancel may race after run resolves. Keep strip status cancelled
576+ // when requested, but never discard a returned body (including salvage).
577+ const wasCancelled =
578+ childCtl . signal . aborted ||
579+ ( session !== undefined &&
580+ deps . sessions ?. get ( session . id ) ?. status === "cancelled" ) ;
581+ const salvage = classifyBriefSalvage ( result ) ;
582+ briefLedger . recordOutcome ( fingerprint , salvage ) ;
583+ const hintOptions = {
584+ dispatchCount,
585+ turnBudgetStopAfterDispatches : TURN_BUDGET_STOP_AFTER_DISPATCHES ,
586+ } ;
587+ if ( wasCancelled ) {
588+ if (
589+ session !== undefined &&
590+ deps . sessions ?. get ( session . id ) ?. status === "running"
591+ ) {
592+ deps . sessions . cancel ( session . id , cancelReason ( childCtl . signal ) ) ;
593+ }
594+ const reported = appendSubAgentParentHints ( result , hintOptions ) ;
595+ return finishWithWorktree (
596+ taskToolResult ( call . id , `Sub-agent "${ description } " reported:\n\n${ reported } ` ) ,
597+ ) ;
593598 }
599+ if ( session !== undefined ) deps . sessions ?. complete ( session . id , result ) ;
594600 const reported = appendSubAgentParentHints ( result , hintOptions ) ;
595601 return finishWithWorktree (
596602 taskToolResult ( call . id , `Sub-agent "${ description } " reported:\n\n${ reported } ` ) ,
597603 ) ;
598- }
599- if ( session !== undefined ) deps . sessions ?. complete ( session . id , result ) ;
600- const reported = appendSubAgentParentHints ( result , hintOptions ) ;
601- return finishWithWorktree (
602- taskToolResult ( call . id , `Sub-agent "${ description } " reported:\n\n${ reported } ` ) ,
603- ) ;
604604
605- } catch ( err ) {
606- if (
607- isSubAgentCancelError ( err , childCtl . signal ) ||
608- ( session !== undefined &&
609- deps . sessions ?. get ( session . id ) ?. status === "cancelled" )
610- ) {
611- briefLedger . recordOutcome ( fingerprint , "cancelled" ) ;
605+ } catch ( err ) {
612606 if (
613- session !== undefined &&
614- deps . sessions ?. get ( session . id ) ?. status === "running"
607+ isSubAgentCancelError ( err , childCtl . signal ) ||
608+ ( session !== undefined &&
609+ deps . sessions ?. get ( session . id ) ?. status === "cancelled" )
615610 ) {
616- deps . sessions . cancel ( session . id , cancelReason ( childCtl . signal ) ) ;
611+ briefLedger . recordOutcome ( fingerprint , "cancelled" ) ;
612+ if (
613+ session !== undefined &&
614+ deps . sessions ?. get ( session . id ) ?. status === "running"
615+ ) {
616+ deps . sessions . cancel ( session . id , cancelReason ( childCtl . signal ) ) ;
617+ }
618+ return finishWithWorktree ( taskToolResult ( call . id , cancelledSubAgentMessage ( description ) ) ) ;
617619 }
618- return finishWithWorktree ( taskToolResult ( call . id , cancelledSubAgentMessage ( description ) ) ) ;
620+ // Run never produced a body — undo the admit so turn-budget retry budget
621+ // is not burned by auth/provider crashes.
622+ briefLedger . release ( fingerprint ) ;
623+ const authMessage = formatSubAgentTaskAuthFailureMessage ( description , err ) ;
624+ const message =
625+ authMessage !== null
626+ ? `Error: ${ authMessage } `
627+ : `Error: sub-agent "${ description } " failed: ${ err instanceof Error ? err . message : String ( err ) } ` ;
628+ const sessionError = err instanceof Error ? err . message : String ( err ) ;
629+ // fail() prefixes "Error:" on the transcript report entry — pass bare text.
630+ const failReason = authMessage ?? sessionError ;
631+ if ( session !== undefined ) deps . sessions ?. fail ( session . id , failReason ) ;
632+ return finishWithWorktree ( taskToolResult ( call . id , message ) ) ;
633+ } finally {
634+ signal . removeEventListener ( "abort" , onParentAbort ) ;
619635 }
620- // Run never produced a body — undo the admit so turn-budget retry budget
621- // is not burned by auth/provider crashes.
622- briefLedger . release ( fingerprint ) ;
623- const authMessage = formatSubAgentTaskAuthFailureMessage ( description , err ) ;
624- const message =
625- authMessage !== null
626- ? `Error: ${ authMessage } `
627- : `Error: sub-agent "${ description } " failed: ${ err instanceof Error ? err . message : String ( err ) } ` ;
628- const sessionError = err instanceof Error ? err . message : String ( err ) ;
629- // fail() prefixes "Error:" on the transcript report entry — pass bare text.
630- const failReason = authMessage ?? sessionError ;
631- if ( session !== undefined ) deps . sessions ?. fail ( session . id , failReason ) ;
632- return finishWithWorktree ( taskToolResult ( call . id , message ) ) ;
633636 } finally {
634- signal . removeEventListener ( "abort" , onParentAbort ) ;
637+ end ( subagentSpanId ) ;
635638 }
636639 } ,
637640 } ) ;
0 commit comments