@@ -53,6 +53,12 @@ interface PendingTaskWaiter {
5353 cleanup : ( ) => void ;
5454}
5555
56+ interface PendingTaskStartWaiter {
57+ createdAt : number ;
58+ start : ( ) => void ;
59+ cleanup : ( ) => void ;
60+ }
61+
5662function isToolCallEndEvent ( value : unknown ) : value is ToolCallEndEvent {
5763 return (
5864 typeof value === "object" &&
@@ -113,6 +119,7 @@ function getIsoNow(): string {
113119export class TaskService {
114120 private readonly mutex = new AsyncMutex ( ) ;
115121 private readonly pendingWaitersByTaskId = new Map < string , PendingTaskWaiter [ ] > ( ) ;
122+ private readonly pendingStartWaitersByTaskId = new Map < string , PendingTaskStartWaiter [ ] > ( ) ;
116123 private readonly remindedAwaitingReport = new Set < string > ( ) ;
117124
118125 constructor (
@@ -416,12 +423,33 @@ export class TaskService {
416423
417424 return new Promise < { reportMarkdown : string ; title ?: string } > ( ( resolve , reject ) => {
418425 let timeout : ReturnType < typeof setTimeout > | null = null ;
426+ let startWaiter : PendingTaskStartWaiter | null = null ;
419427 let abortListener : ( ( ) => void ) | null = null ;
420428
429+ const startReportTimeout = ( ) => {
430+ if ( timeout ) return ;
431+ timeout = setTimeout ( ( ) => {
432+ entry . cleanup ( ) ;
433+ reject ( new Error ( "Timed out waiting for agent_report" ) ) ;
434+ } , timeoutMs ) ;
435+ } ;
436+
437+ const cleanupStartWaiter = ( ) => {
438+ if ( ! startWaiter ) return ;
439+ startWaiter . cleanup ( ) ;
440+ startWaiter = null ;
441+ } ;
442+
421443 const entry : PendingTaskWaiter = {
422444 createdAt : Date . now ( ) ,
423- resolve,
424- reject,
445+ resolve : ( report ) => {
446+ entry . cleanup ( ) ;
447+ resolve ( report ) ;
448+ } ,
449+ reject : ( error ) => {
450+ entry . cleanup ( ) ;
451+ reject ( error ) ;
452+ } ,
425453 cleanup : ( ) => {
426454 const current = this . pendingWaitersByTaskId . get ( taskId ) ;
427455 if ( current ) {
@@ -433,6 +461,8 @@ export class TaskService {
433461 }
434462 }
435463
464+ cleanupStartWaiter ( ) ;
465+
436466 if ( timeout ) {
437467 clearTimeout ( timeout ) ;
438468 timeout = null ;
@@ -449,10 +479,43 @@ export class TaskService {
449479 list . push ( entry ) ;
450480 this . pendingWaitersByTaskId . set ( taskId , list ) ;
451481
452- timeout = setTimeout ( ( ) => {
453- entry . cleanup ( ) ;
454- reject ( new Error ( "Timed out waiting for agent_report" ) ) ;
455- } , timeoutMs ) ;
482+ // Don't start the execution timeout while the task is still queued.
483+ // The timer starts once the child actually begins running (queued -> running).
484+ const cfg = this . config . loadConfigOrDefault ( ) ;
485+ const taskEntry = this . findWorkspaceEntry ( cfg , taskId ) ;
486+ const initialStatus = taskEntry ?. workspace . taskStatus ;
487+ if ( initialStatus === "queued" ) {
488+ const startWaiterEntry : PendingTaskStartWaiter = {
489+ createdAt : Date . now ( ) ,
490+ start : startReportTimeout ,
491+ cleanup : ( ) => {
492+ const currentStartWaiters = this . pendingStartWaitersByTaskId . get ( taskId ) ;
493+ if ( currentStartWaiters ) {
494+ const next = currentStartWaiters . filter ( ( w ) => w !== startWaiterEntry ) ;
495+ if ( next . length === 0 ) {
496+ this . pendingStartWaitersByTaskId . delete ( taskId ) ;
497+ } else {
498+ this . pendingStartWaitersByTaskId . set ( taskId , next ) ;
499+ }
500+ }
501+ } ,
502+ } ;
503+ startWaiter = startWaiterEntry ;
504+
505+ const currentStartWaiters = this . pendingStartWaitersByTaskId . get ( taskId ) ?? [ ] ;
506+ currentStartWaiters . push ( startWaiterEntry ) ;
507+ this . pendingStartWaitersByTaskId . set ( taskId , currentStartWaiters ) ;
508+
509+ // Close the race where the task starts between the initial config read and registering the waiter.
510+ const cfgAfterRegister = this . config . loadConfigOrDefault ( ) ;
511+ const afterEntry = this . findWorkspaceEntry ( cfgAfterRegister , taskId ) ;
512+ if ( afterEntry ?. workspace . taskStatus !== "queued" ) {
513+ cleanupStartWaiter ( ) ;
514+ startReportTimeout ( ) ;
515+ }
516+ } else {
517+ startReportTimeout ( ) ;
518+ }
456519
457520 if ( options ?. abortSignal ) {
458521 if ( options . abortSignal . aborted ) {
@@ -578,8 +641,6 @@ export class TaskService {
578641 for ( const task of queued ) {
579642 if ( ! task . id ) continue ;
580643
581- await this . setTaskStatus ( task . id , "running" ) ;
582-
583644 // Start by resuming from the queued prompt in history.
584645 const model = task . taskModelString ?? defaultModel ;
585646 const resumeResult = await this . workspaceService . resumeStream ( task . id , {
@@ -589,9 +650,10 @@ export class TaskService {
589650
590651 if ( ! resumeResult . success ) {
591652 log . error ( "Failed to start queued task" , { taskId : task . id , error : resumeResult . error } ) ;
592- // Put it back in queued to retry later.
593- await this . setTaskStatus ( task . id , "queued" ) ;
653+ continue ;
594654 }
655+
656+ await this . setTaskStatus ( task . id , "running" ) ;
595657 }
596658 }
597659
@@ -612,6 +674,19 @@ export class TaskService {
612674 const allMetadata = await this . config . getAllWorkspaceMetadata ( ) ;
613675 const metadata = allMetadata . find ( ( m ) => m . id === workspaceId ) ?? null ;
614676 this . workspaceService . emit ( "metadata" , { workspaceId, metadata } ) ;
677+
678+ if ( status === "running" ) {
679+ const waiters = this . pendingStartWaitersByTaskId . get ( workspaceId ) ;
680+ if ( ! waiters || waiters . length === 0 ) return ;
681+ this . pendingStartWaitersByTaskId . delete ( workspaceId ) ;
682+ for ( const waiter of waiters ) {
683+ try {
684+ waiter . start ( ) ;
685+ } catch ( error : unknown ) {
686+ log . error ( "Task start waiter callback failed" , { workspaceId, error } ) ;
687+ }
688+ }
689+ }
615690 }
616691
617692 private async handleStreamEnd ( event : StreamEndEvent ) : Promise < void > {
0 commit comments