1010 * This is Phase 4 of the parallel execution roadmap: Background Read-Only Concurrency.
1111 */
1212
13+ import { BackgroundTaskStatusInfo } from "@roo-code/types"
14+
1315import { Task , TaskOptions } from "./Task"
1416
1517/** Read-only tools that background tasks are allowed to use. */
@@ -46,11 +48,27 @@ export interface BackgroundTaskRunnerCallbacks {
4648 onTaskError ?: ( taskId : string , parentTaskId : string , error : Error ) => void
4749}
4850
51+ /** Maximum number of recently completed tasks to keep for UI display. */
52+ const MAX_COMPLETED_TASKS = 10
53+
54+ export interface CompletedBackgroundTaskInfo {
55+ taskId : string
56+ parentTaskId : string
57+ status : "completed" | "cancelled" | "timed_out" | "error"
58+ startedAt : number
59+ completedAt : number
60+ resultSummary ?: string
61+ mode ?: string
62+ }
63+
4964export class BackgroundTaskRunner {
5065 private backgroundTasks : Map < string , BackgroundTaskInfo > = new Map ( )
66+ private completedTasks : CompletedBackgroundTaskInfo [ ] = [ ]
5167 private maxConcurrentTasks : number
5268 private taskTimeoutMs : number
5369 private callbacks : BackgroundTaskRunnerCallbacks
70+ /** Called whenever the set of active/completed tasks changes, so the UI can be refreshed. */
71+ public onStateChanged ?: ( ) => void
5472
5573 constructor (
5674 maxConcurrentTasks : number = DEFAULT_MAX_BACKGROUND_TASKS ,
@@ -108,12 +126,14 @@ export class BackgroundTaskRunner {
108126 `[BackgroundTaskRunner] Registered background task ${ task . taskId } ` +
109127 `(parent: ${ parentTaskId } , active: ${ this . backgroundTasks . size } /${ this . maxConcurrentTasks } )` ,
110128 )
129+
130+ this . notifyStateChanged ( )
111131 }
112132
113133 /**
114134 * Called when a background task completes. Cleans up tracking state.
115135 */
116- onTaskCompleted ( taskId : string ) : BackgroundTaskInfo | undefined {
136+ onTaskCompleted ( taskId : string , resultSummary ?: string ) : BackgroundTaskInfo | undefined {
117137 const info = this . backgroundTasks . get ( taskId )
118138
119139 if ( ! info ) {
@@ -123,11 +143,22 @@ export class BackgroundTaskRunner {
123143 clearTimeout ( info . timeoutHandle )
124144 this . backgroundTasks . delete ( taskId )
125145
146+ this . addCompletedTask ( {
147+ taskId,
148+ parentTaskId : info . parentTaskId ,
149+ status : "completed" ,
150+ startedAt : info . startedAt ,
151+ completedAt : Date . now ( ) ,
152+ resultSummary,
153+ } )
154+
126155 console . log (
127156 `[BackgroundTaskRunner] Background task ${ taskId } completed ` +
128157 `(active: ${ this . backgroundTasks . size } /${ this . maxConcurrentTasks } )` ,
129158 )
130159
160+ this . notifyStateChanged ( )
161+
131162 return info
132163 }
133164
@@ -174,9 +205,12 @@ export class BackgroundTaskRunner {
174205
175206 clearTimeout ( info . timeoutHandle )
176207
208+ let status : CompletedBackgroundTaskInfo [ "status" ] = "cancelled"
209+
177210 try {
178211 await info . task . abortTask ( true )
179212 } catch ( error ) {
213+ status = "error"
180214 const err = error instanceof Error ? error : new Error ( String ( error ) )
181215 console . error ( `[BackgroundTaskRunner] Error aborting background task ${ taskId } : ${ err . message } ` )
182216 try {
@@ -188,10 +222,20 @@ export class BackgroundTaskRunner {
188222
189223 this . backgroundTasks . delete ( taskId )
190224
225+ this . addCompletedTask ( {
226+ taskId,
227+ parentTaskId : info . parentTaskId ,
228+ status,
229+ startedAt : info . startedAt ,
230+ completedAt : Date . now ( ) ,
231+ } )
232+
191233 console . log (
192234 `[BackgroundTaskRunner] Cancelled background task ${ taskId } ` +
193235 `(active: ${ this . backgroundTasks . size } /${ this . maxConcurrentTasks } )` ,
194236 )
237+
238+ this . notifyStateChanged ( )
195239 }
196240
197241 /**
@@ -205,12 +249,79 @@ export class BackgroundTaskRunner {
205249 }
206250 }
207251
252+ /**
253+ * Returns the combined status of all active and recently completed background tasks
254+ * for display in the webview UI.
255+ */
256+ getTasksStatus ( ) : BackgroundTaskStatusInfo [ ] {
257+ const activeTasks : BackgroundTaskStatusInfo [ ] = [ ]
258+
259+ for ( const [ taskId , info ] of this . backgroundTasks ) {
260+ activeTasks . push ( {
261+ taskId,
262+ parentTaskId : info . parentTaskId ,
263+ status : "running" ,
264+ startedAt : info . startedAt ,
265+ } )
266+ }
267+
268+ const completedStatuses : BackgroundTaskStatusInfo [ ] = this . completedTasks . map ( ( ct ) => ( {
269+ taskId : ct . taskId ,
270+ parentTaskId : ct . parentTaskId ,
271+ status : ct . status ,
272+ startedAt : ct . startedAt ,
273+ completedAt : ct . completedAt ,
274+ resultSummary : ct . resultSummary ,
275+ mode : ct . mode ,
276+ } ) )
277+
278+ return [ ...activeTasks , ...completedStatuses ]
279+ }
280+
281+ /**
282+ * Returns the list of recently completed tasks (for testing and direct access).
283+ */
284+ getCompletedTasks ( ) : readonly CompletedBackgroundTaskInfo [ ] {
285+ return this . completedTasks
286+ }
287+
288+ /**
289+ * Clears completed tasks from the buffer.
290+ */
291+ clearCompletedTasks ( ) : void {
292+ this . completedTasks = [ ]
293+ this . notifyStateChanged ( )
294+ }
295+
296+ /**
297+ * Add a completed task to the buffer, evicting the oldest if at capacity.
298+ */
299+ private addCompletedTask ( info : CompletedBackgroundTaskInfo ) : void {
300+ this . completedTasks . push ( info )
301+
302+ if ( this . completedTasks . length > MAX_COMPLETED_TASKS ) {
303+ this . completedTasks = this . completedTasks . slice ( - MAX_COMPLETED_TASKS )
304+ }
305+ }
306+
307+ /**
308+ * Notify the owner that background task state has changed.
309+ */
310+ private notifyStateChanged ( ) : void {
311+ try {
312+ this . onStateChanged ?.( )
313+ } catch {
314+ // Callback errors must not break internal logic.
315+ }
316+ }
317+
208318 /**
209319 * Handle timeout of a background task.
210320 */
211321 private async timeoutTask ( taskId : string ) : Promise < void > {
212322 const info = this . backgroundTasks . get ( taskId )
213323 const parentTaskId = info ?. parentTaskId ?? "unknown"
324+ const startedAt = info ?. startedAt ?? Date . now ( )
214325
215326 console . warn ( `[BackgroundTaskRunner] Background task ${ taskId } timed out after ${ this . taskTimeoutMs } ms` )
216327
@@ -220,6 +331,28 @@ export class BackgroundTaskRunner {
220331 // Callback errors must not break cleanup.
221332 }
222333
223- await this . cancelTask ( taskId )
334+ // Record as timed_out before cancelling (cancelTask will record as cancelled otherwise)
335+ clearTimeout ( info ?. timeoutHandle )
336+ if ( info ) {
337+ try {
338+ await info . task . abortTask ( true )
339+ } catch ( error ) {
340+ const err = error instanceof Error ? error : new Error ( String ( error ) )
341+ console . error ( `[BackgroundTaskRunner] Error aborting timed-out task ${ taskId } : ${ err . message } ` )
342+ }
343+ this . backgroundTasks . delete ( taskId )
344+
345+ this . addCompletedTask ( {
346+ taskId,
347+ parentTaskId,
348+ status : "timed_out" ,
349+ startedAt,
350+ completedAt : Date . now ( ) ,
351+ } )
352+
353+ this . notifyStateChanged ( )
354+ } else {
355+ await this . cancelTask ( taskId )
356+ }
224357 }
225358}
0 commit comments