@@ -6,6 +6,7 @@ import { v7 as uuidv7 } from "uuid"
66import EventEmitter from "events"
77
88import { AskIgnoredError } from "./AskIgnoredError"
9+ import { RateLimitClock , createRateLimitClock } from "./RateLimitClock"
910
1011import { Anthropic } from "@anthropic-ai/sdk"
1112import OpenAI from "openai"
@@ -158,6 +159,7 @@ export interface TaskOptions extends CreateTaskOptions {
158159 workspacePath ?: string
159160 /** Initial status for the task's history item (e.g., "active" for child tasks) */
160161 initialStatus ?: "active" | "delegated" | "completed"
162+ rateLimitClock ?: RateLimitClock
161163}
162164
163165export class Task extends EventEmitter < TaskEvents > implements TaskLike {
@@ -284,17 +286,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
284286 // API
285287 apiConfiguration : ProviderSettings
286288 api : ApiHandler
287- private static lastGlobalApiRequestTime ?: number
289+ private rateLimitClock : RateLimitClock
288290 private autoApprovalHandler : AutoApprovalHandler
289291
290- /**
291- * Reset the global API request timestamp. This should only be used for testing.
292- * @internal
293- */
294- static resetGlobalApiRequestTime ( ) : void {
295- Task . lastGlobalApiRequestTime = undefined
296- }
297-
298292 toolRepetitionDetector : ToolRepetitionDetector
299293 rooIgnoreController ?: RooIgnoreController
300294 rooProtectedController ?: RooProtectedController
@@ -437,6 +431,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
437431 initialTodos,
438432 workspacePath,
439433 initialStatus,
434+ rateLimitClock,
440435 } : TaskOptions ) {
441436 super ( )
442437
@@ -486,6 +481,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
486481
487482 this . apiConfiguration = apiConfiguration
488483 this . api = buildApiHandler ( this . apiConfiguration )
484+ this . rateLimitClock = rateLimitClock ?? createRateLimitClock ( )
489485 this . autoApprovalHandler = new AutoApprovalHandler ( )
490486
491487 this . consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
@@ -2455,12 +2451,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
24552451 // This prevents the UI from showing an "API Request..." spinner while we are
24562452 // intentionally waiting due to the rate limit slider.
24572453 //
2458- // NOTE: We also set Task.lastGlobalApiRequestTime here to reserve this slot
2459- // before we build environment details (which can take time).
2460- // This ensures subsequent requests (including subtasks) still honour the
2454+ // NOTE: We also record the request time here to reserve this slot before
2455+ // we build environment details (which can take time). This ensures
2456+ // subsequent requests (including subtasks) still honour the
24612457 // provider rate-limit window.
24622458 await this . maybeWaitForProviderRateLimit ( currentItem . retryAttempt ?? 0 )
2463- Task . lastGlobalApiRequestTime = performance . now ( )
2459+ this . rateLimitClock . recordRequest ( )
24642460
24652461 await this . say (
24662462 "api_req_started" ,
@@ -3854,12 +3850,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
38543850 const rateLimitSeconds =
38553851 state ?. apiConfiguration ?. rateLimitSeconds ?? this . apiConfiguration ?. rateLimitSeconds ?? 0
38563852
3857- if ( rateLimitSeconds <= 0 || ! Task . lastGlobalApiRequestTime ) {
3853+ const lastRequestTime = this . rateLimitClock . getLastRequestTime ( )
3854+ if ( rateLimitSeconds <= 0 || ! lastRequestTime ) {
38583855 return
38593856 }
38603857
38613858 const now = performance . now ( )
3862- const timeSinceLastRequest = now - Task . lastGlobalApiRequestTime
3859+ const timeSinceLastRequest = now - lastRequestTime
38633860 const rateLimitDelay = Math . ceil (
38643861 Math . min ( rateLimitSeconds , Math . max ( 0 , rateLimitSeconds * 1000 - timeSinceLastRequest ) / 1000 ) ,
38653862 )
@@ -3907,7 +3904,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
39073904 // timestamp earlier to include the environment details build. We still set it
39083905 // here for direct callers (tests) and for the case where we didn't rate-limit
39093906 // in the caller.
3910- Task . lastGlobalApiRequestTime = performance . now ( )
3907+ this . rateLimitClock . recordRequest ( )
39113908
39123909 const systemPrompt = await this . getSystemPrompt ( )
39133910 const { contextTokens } = this . getTokenUsage ( )
@@ -4282,8 +4279,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
42824279 // Respect provider rate limit window
42834280 let rateLimitDelay = 0
42844281 const rateLimit = ( state ?. apiConfiguration ?? this . apiConfiguration ) ?. rateLimitSeconds || 0
4285- if ( Task . lastGlobalApiRequestTime && rateLimit > 0 ) {
4286- const elapsed = performance . now ( ) - Task . lastGlobalApiRequestTime
4282+ const lastRequestTime = this . rateLimitClock . getLastRequestTime ( )
4283+ if ( lastRequestTime && rateLimit > 0 ) {
4284+ const elapsed = performance . now ( ) - lastRequestTime
42874285 rateLimitDelay = Math . ceil ( Math . min ( rateLimit , Math . max ( 0 , rateLimit * 1000 - elapsed ) / 1000 ) )
42884286 }
42894287
0 commit comments