@@ -162,6 +162,10 @@ export class TaskService {
162162 private readonly mutex = new AsyncMutex ( ) ;
163163 private readonly pendingWaitersByTaskId = new Map < string , PendingTaskWaiter [ ] > ( ) ;
164164 private readonly pendingStartWaitersByTaskId = new Map < string , PendingTaskStartWaiter [ ] > ( ) ;
165+ // Tracks workspaces currently blocked in a foreground wait (e.g. a task tool call awaiting
166+ // agent_report). Used to avoid scheduler deadlocks when maxParallelAgentTasks is low and tasks
167+ // spawn nested tasks in the foreground.
168+ private readonly foregroundAwaitCountByWorkspaceId = new Map < string , number > ( ) ;
165169 // Cache completed reports so callers can retrieve them even after the task workspace is removed.
166170 // Bounded by TTL + max entries (see COMPLETED_REPORT_CACHE_*).
167171 private readonly completedReportsByTaskId = new Map <
@@ -682,9 +686,39 @@ export class TaskService {
682686 }
683687 }
684688
689+ private isForegroundAwaiting ( workspaceId : string ) : boolean {
690+ const count = this . foregroundAwaitCountByWorkspaceId . get ( workspaceId ) ;
691+ return typeof count === "number" && count > 0 ;
692+ }
693+
694+ private startForegroundAwait ( workspaceId : string ) : ( ) => void {
695+ assert ( workspaceId . length > 0 , "startForegroundAwait: workspaceId must be non-empty" ) ;
696+
697+ const current = this . foregroundAwaitCountByWorkspaceId . get ( workspaceId ) ?? 0 ;
698+ assert (
699+ Number . isInteger ( current ) && current >= 0 ,
700+ "startForegroundAwait: expected non-negative integer counter"
701+ ) ;
702+
703+ this . foregroundAwaitCountByWorkspaceId . set ( workspaceId , current + 1 ) ;
704+
705+ return ( ) => {
706+ const current = this . foregroundAwaitCountByWorkspaceId . get ( workspaceId ) ?? 0 ;
707+ assert (
708+ Number . isInteger ( current ) && current > 0 ,
709+ "startForegroundAwait cleanup: expected positive integer counter"
710+ ) ;
711+ if ( current <= 1 ) {
712+ this . foregroundAwaitCountByWorkspaceId . delete ( workspaceId ) ;
713+ } else {
714+ this . foregroundAwaitCountByWorkspaceId . set ( workspaceId , current - 1 ) ;
715+ }
716+ } ;
717+ }
718+
685719 waitForAgentReport (
686720 taskId : string ,
687- options ?: { timeoutMs ?: number ; abortSignal ?: AbortSignal }
721+ options ?: { timeoutMs ?: number ; abortSignal ?: AbortSignal ; requestingWorkspaceId ?: string }
688722 ) : Promise < { reportMarkdown : string ; title ?: string } > {
689723 assert ( taskId . length > 0 , "waitForAgentReport: taskId must be non-empty" ) ;
690724
@@ -700,6 +734,8 @@ export class TaskService {
700734 const timeoutMs = options ?. timeoutMs ?? 10 * 60 * 1000 ; // 10 minutes
701735 assert ( Number . isFinite ( timeoutMs ) && timeoutMs > 0 , "waitForAgentReport: timeoutMs invalid" ) ;
702736
737+ const requestingWorkspaceId = coerceNonEmptyString ( options ?. requestingWorkspaceId ) ;
738+
703739 return new Promise < { reportMarkdown : string ; title ?: string } > ( ( resolve , reject ) => {
704740 // Validate existence early to avoid waiting on never-resolving task IDs.
705741 const cfg = this . config . loadConfigOrDefault ( ) ;
@@ -712,6 +748,9 @@ export class TaskService {
712748 let timeout : ReturnType < typeof setTimeout > | null = null ;
713749 let startWaiter : PendingTaskStartWaiter | null = null ;
714750 let abortListener : ( ( ) => void ) | null = null ;
751+ let stopBlockingRequester : ( ( ) => void ) | null = requestingWorkspaceId
752+ ? this . startForegroundAwait ( requestingWorkspaceId )
753+ : null ;
715754
716755 const startReportTimeout = ( ) => {
717756 if ( timeout ) return ;
@@ -759,6 +798,14 @@ export class TaskService {
759798 options . abortSignal . removeEventListener ( "abort" , abortListener ) ;
760799 abortListener = null ;
761800 }
801+
802+ if ( stopBlockingRequester ) {
803+ try {
804+ stopBlockingRequester ( ) ;
805+ } finally {
806+ stopBlockingRequester = null ;
807+ }
808+ }
762809 } ,
763810 } ;
764811
@@ -798,6 +845,13 @@ export class TaskService {
798845 cleanupStartWaiter ( ) ;
799846 startReportTimeout ( ) ;
800847 }
848+
849+ // If the awaited task is queued and the caller is blocked in the foreground, ensure the
850+ // scheduler runs after the waiter is registered. This avoids deadlocks when
851+ // maxParallelAgentTasks is low.
852+ if ( requestingWorkspaceId ) {
853+ void this . maybeStartQueuedTasks ( ) ;
854+ }
801855 } else {
802856 startReportTimeout ( ) ;
803857 }
@@ -1040,6 +1094,14 @@ export class TaskService {
10401094 let activeCount = 0 ;
10411095 for ( const task of this . listAgentTaskWorkspaces ( config ) ) {
10421096 const status : AgentTaskStatus = task . taskStatus ?? "running" ;
1097+ // If this task workspace is blocked in a foreground wait, do not count it towards parallelism.
1098+ // This prevents deadlocks where a task spawns a nested task in the foreground while
1099+ // maxParallelAgentTasks is low (e.g. 1).
1100+ // Note: StreamManager can still report isStreaming() while a tool call is executing, so
1101+ // isStreaming is not a reliable signal for "actively doing work" here.
1102+ if ( status === "running" && task . id && this . isForegroundAwaiting ( task . id ) ) {
1103+ continue ;
1104+ }
10431105 if ( status === "running" || status === "awaiting_report" ) {
10441106 activeCount += 1 ;
10451107 continue ;
0 commit comments