(retryAttempt: number, error: any)
| 4384 | |
| 4385 | // Shared exponential backoff for retries (first-chunk and mid-stream) |
| 4386 | private async backoffAndAnnounce(retryAttempt: number, error: any): Promise<void> { |
| 4387 | try { |
| 4388 | const state = await this.providerRef.deref()?.getState() |
| 4389 | const baseDelay = state?.requestDelaySeconds || 5 |
| 4390 | |
| 4391 | let exponentialDelay = Math.min( |
| 4392 | Math.ceil(baseDelay * Math.pow(2, retryAttempt)), |
| 4393 | MAX_EXPONENTIAL_BACKOFF_SECONDS, |
| 4394 | ) |
| 4395 | |
| 4396 | // Respect provider rate limit window |
| 4397 | let rateLimitDelay = 0 |
| 4398 | const rateLimit = (state?.apiConfiguration ?? this.apiConfiguration)?.rateLimitSeconds || 0 |
| 4399 | const lastRequestTime = this.rateLimitClock.getLastRequestTime() |
| 4400 | if (lastRequestTime && rateLimit > 0) { |
| 4401 | const elapsed = performance.now() - lastRequestTime |
| 4402 | rateLimitDelay = Math.ceil(Math.min(rateLimit, Math.max(0, rateLimit * 1000 - elapsed) / 1000)) |
| 4403 | } |
| 4404 | |
| 4405 | // Prefer RetryInfo on 429 if present |
| 4406 | if (error?.status === 429) { |
| 4407 | const retryInfo = error?.errorDetails?.find( |
| 4408 | (d: any) => d["@type"] === "type.googleapis.com/google.rpc.RetryInfo", |
| 4409 | ) |
| 4410 | const match = retryInfo?.retryDelay?.match?.(/^(\d+)s$/) |
| 4411 | if (match) { |
| 4412 | exponentialDelay = Number(match[1]) + 1 |
| 4413 | } |
| 4414 | } |
| 4415 | |
| 4416 | const finalDelay = Math.max(exponentialDelay, rateLimitDelay) |
| 4417 | if (finalDelay <= 0) { |
| 4418 | return |
| 4419 | } |
| 4420 | |
| 4421 | // Build header text; fall back to error message if none provided |
| 4422 | let headerText |
| 4423 | if (error.status) { |
| 4424 | // Include both status code (for ChatRow parsing) and detailed message (for error details) |
| 4425 | // Format: "<status>\n<message>" allows ChatRow to extract status via parseInt(text.substring(0,3)) |
| 4426 | // while preserving the full error message in errorDetails for debugging |
| 4427 | const errorMessage = error?.message || "Unknown error" |
| 4428 | headerText = `${error.status}\n${errorMessage}` |
| 4429 | } else if (error?.message) { |
| 4430 | headerText = error.message |
| 4431 | } else { |
| 4432 | headerText = "Unknown error" |
| 4433 | } |
| 4434 | |
| 4435 | headerText = headerText ? `${headerText}\n` : "" |
| 4436 | |
| 4437 | // Show countdown timer with exponential backoff |
| 4438 | for (let i = finalDelay; i > 0; i--) { |
| 4439 | // Check abort flag during countdown to allow early exit |
| 4440 | if (this.abort) { |
| 4441 | throw new Error(`[Task#${this.taskId}] Aborted during retry countdown`) |
| 4442 | } |
| 4443 |
no test coverage detected