Spaces:
Sleeping
Sleeping
yc1838 commited on
Commit ·
259c7bc
1
Parent(s): 7aa5b26
docs: refine gemini cooldown design
Browse files
docs/superpowers/specs/2026-05-06-gemini-rate-limit-cooldown-design.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
---
|
| 2 |
title: Gemini Rate-Limit Cooldown Design
|
| 3 |
date: 2026-05-06
|
| 4 |
-
status:
|
| 5 |
---
|
| 6 |
|
| 7 |
# Gemini Rate-Limit Cooldown Design
|
|
@@ -10,6 +10,8 @@ status: approved-for-planning
|
|
| 10 |
|
| 11 |
Lilith currently wraps chat model calls in `_RetryWrapper` with tenacity exponential backoff for provider rate-limit exceptions. This protects a single model call, but it does not coordinate across calls or GAIA tasks. If Gemini quota pressure persists after retries are exhausted, the runner can move to the next task and immediately repeat the same retry ladder against the same overloaded model lane.
|
| 12 |
|
|
|
|
|
|
|
| 13 |
For current usage, only two Google text models need first-class cooldown behavior:
|
| 14 |
|
| 15 |
| Model | RPM | TPM | RPD |
|
|
@@ -21,11 +23,13 @@ Gemma, embeddings, image, audio, video, and grounding quotas are out of scope fo
|
|
| 21 |
|
| 22 |
## Goals
|
| 23 |
|
| 24 |
-
1.
|
| 25 |
-
2.
|
| 26 |
-
3.
|
| 27 |
-
4.
|
| 28 |
-
5.
|
|
|
|
|
|
|
| 29 |
|
| 30 |
## Non-goals
|
| 31 |
|
|
@@ -45,7 +49,40 @@ Represent each rate-limited Gemini model as a lane keyed by provider and model:
|
|
| 45 |
- `google:gemini-3-flash-preview`
|
| 46 |
- `google:gemini-3.1-pro`
|
| 47 |
|
| 48 |
-
Unknown models should continue to use
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
### In-process cooldown registry
|
| 51 |
|
|
@@ -56,26 +93,25 @@ Add a small module-level registry in `models.py`:
|
|
| 56 |
(provider, model) -> consecutive_rate_limit_exhaustions
|
| 57 |
```
|
| 58 |
|
|
|
|
|
|
|
| 59 |
Before invoking the wrapped model, `_RetryWrapper` checks whether its lane has an active cooldown. If active, it sleeps until the cooldown expires, then proceeds with the existing tenacity retry loop.
|
| 60 |
|
| 61 |
After a successful call, the lane's consecutive rate-limit exhaustion count resets to zero.
|
| 62 |
|
| 63 |
-
|
| 64 |
|
| 65 |
-
|
| 66 |
|
| 67 |
-
The
|
| 68 |
|
| 69 |
-
|
| 70 |
-
-
|
| 71 |
-
|
| 72 |
-
- original exception string
|
| 73 |
-
|
| 74 |
-
Non-rate-limit exceptions should keep their existing behavior and should not be rewritten as cooldown errors.
|
| 75 |
|
| 76 |
-
|
| 77 |
|
| 78 |
-
|
| 79 |
|
| 80 |
| Consecutive exhausted rate limits | Cooldown |
|
| 81 |
| ---: | ---: |
|
|
@@ -83,72 +119,158 @@ Because both target models have high RPM, exhausted 429s are likely burst, token
|
|
| 83 |
| 2 | 120 seconds |
|
| 84 |
| 3+ | 300 seconds |
|
| 85 |
|
| 86 |
-
If
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
### Runner behavior
|
| 89 |
|
| 90 |
-
`runner.py::run_agent_on_questions`
|
| 91 |
|
| 92 |
For each GAIA task:
|
| 93 |
|
| 94 |
-
1. Invoke the graph normally.
|
| 95 |
2. If a `RateLimitCooldownError` escapes:
|
| 96 |
- log the task, model, and cooldown duration,
|
| 97 |
- sleep for the indicated cooldown,
|
| 98 |
-
- retry the same task once
|
| 99 |
3. If the retry succeeds, format and checkpoint the answer normally.
|
| 100 |
4. If the retry still fails with rate-limit exhaustion, append an `AGENT ERROR: RATE LIMITED` answer for this run and continue to the next question.
|
| 101 |
-
5.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
|
| 104 |
|
| 105 |
## Data Flow
|
| 106 |
|
| 107 |
```text
|
| 108 |
GAIA task
|
|
|
|
| 109 |
-> runner invokes graph
|
| 110 |
-> model node calls Gemini lane
|
| 111 |
-> _RetryWrapper checks lane cooldown
|
| 112 |
-
-> tenacity retries
|
| 113 |
-
->
|
|
|
|
| 114 |
-> RateLimitCooldownError bubbles up
|
| 115 |
-> runner waits cooldown
|
| 116 |
-
-> runner retries same GAIA task once
|
| 117 |
-> success checkpoints answer OR failure continues without success checkpoint
|
| 118 |
```
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
## Error Handling
|
| 121 |
|
| 122 |
-
- Rate-limit exhaustion should be logged at warning level with provider, model, and
|
| 123 |
- Generic graph/model/tool errors should continue through the existing `AGENT ERROR` path.
|
| 124 |
- A failed retry after cooldown should not crash the whole batch.
|
| 125 |
- A successful call after cooldown should reset that model lane's consecutive rate-limit count.
|
|
|
|
|
|
|
| 126 |
|
| 127 |
## Testing Plan
|
| 128 |
|
| 129 |
Add focused unit tests without hitting external APIs:
|
| 130 |
|
| 131 |
-
1. `
|
| 132 |
-
2. `
|
| 133 |
-
3.
|
| 134 |
-
4.
|
| 135 |
-
5.
|
| 136 |
-
6.
|
| 137 |
-
7.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
## Risks and Mitigations
|
| 140 |
|
| 141 |
- **Risk: sleeping in tests slows the suite.**
|
| 142 |
- Mitigation: inject or monkeypatch sleep/time functions in tests.
|
| 143 |
|
|
|
|
|
|
|
|
|
|
| 144 |
- **Risk: cooldown is only in-process.**
|
| 145 |
-
- Mitigation: acceptable for current local batch runs;
|
| 146 |
|
| 147 |
- **Risk: same-question retry duplicates some work.**
|
| 148 |
- Mitigation: retry only once and use a fresh ephemeral memory store, matching existing per-task isolation.
|
| 149 |
|
| 150 |
-
- **Risk:
|
| 151 |
-
- Mitigation:
|
| 152 |
|
| 153 |
## Implementation Boundaries
|
| 154 |
|
|
|
|
| 1 |
---
|
| 2 |
title: Gemini Rate-Limit Cooldown Design
|
| 3 |
date: 2026-05-06
|
| 4 |
+
status: revised-for-review
|
| 5 |
---
|
| 6 |
|
| 7 |
# Gemini Rate-Limit Cooldown Design
|
|
|
|
| 10 |
|
| 11 |
Lilith currently wraps chat model calls in `_RetryWrapper` with tenacity exponential backoff for provider rate-limit exceptions. This protects a single model call, but it does not coordinate across calls or GAIA tasks. If Gemini quota pressure persists after retries are exhausted, the runner can move to the next task and immediately repeat the same retry ladder against the same overloaded model lane.
|
| 12 |
|
| 13 |
+
Current Gemini retry detection also needs tightening. The installed `langchain-google-genai` path catches `google.genai.errors.ClientError`, while `models.py` currently only registers `google.api_core.exceptions.ResourceExhausted` for Google. A cooldown layer must first classify real Gemini 429s correctly, without retrying non-429 client errors such as `400 INVALID_ARGUMENT`.
|
| 14 |
+
|
| 15 |
For current usage, only two Google text models need first-class cooldown behavior:
|
| 16 |
|
| 17 |
| Model | RPM | TPM | RPD |
|
|
|
|
| 23 |
|
| 24 |
## Goals
|
| 25 |
|
| 26 |
+
1. Correctly detect retryable Gemini 429s from the actual GenAI client exception shape.
|
| 27 |
+
2. Retain per-call exponential backoff for transient single-call hiccups.
|
| 28 |
+
3. Add shared lane cooldown for persistent retry exhaustion.
|
| 29 |
+
4. Skip a single GAIA question when it hits the per-question 429 streak threshold.
|
| 30 |
+
5. Pause the whole batch when the recent cross-task 429 rate is high.
|
| 31 |
+
6. Stop the batch on daily-quota/RPD exhaustion instead of polling every few minutes.
|
| 32 |
+
7. Preserve normal non-rate-limit error handling.
|
| 33 |
|
| 34 |
## Non-goals
|
| 35 |
|
|
|
|
| 49 |
- `google:gemini-3-flash-preview`
|
| 50 |
- `google:gemini-3.1-pro`
|
| 51 |
|
| 52 |
+
Unknown models should continue to use generic retry behavior. They must not inherit Gemini-specific cooldown profiles or Gemini quota assumptions.
|
| 53 |
+
|
| 54 |
+
### Retryable rate-limit predicate
|
| 55 |
+
|
| 56 |
+
Replace type-only retry classification with a custom predicate, for example `is_retryable_rate_limit(exc)`.
|
| 57 |
+
|
| 58 |
+
The predicate should return true for:
|
| 59 |
+
|
| 60 |
+
- `google.api_core.exceptions.ResourceExhausted`
|
| 61 |
+
- `google.genai.errors.ClientError` only when `exc.code == 429`
|
| 62 |
+
- Anthropic `RateLimitError`
|
| 63 |
+
- OpenAI `RateLimitError`
|
| 64 |
+
|
| 65 |
+
The predicate should return false for:
|
| 66 |
+
|
| 67 |
+
- `google.genai.errors.ClientError` with any non-429 code
|
| 68 |
+
- generic `ClientError`/`ValueError`/tool errors
|
| 69 |
+
- invalid request errors such as `400 INVALID_ARGUMENT`
|
| 70 |
+
|
| 71 |
+
Tenacity should use this predicate rather than `retry_if_exception_type(RETRY_EXCEPTIONS)`. This keeps 429 handling broad enough for real Gemini calls while preventing retries/cooldowns for deterministic bad requests.
|
| 72 |
+
|
| 73 |
+
### Quota metadata classification
|
| 74 |
+
|
| 75 |
+
When a retryable Gemini 429 is observed, inspect available metadata before deciding whether to apply a short cooldown.
|
| 76 |
+
|
| 77 |
+
Use defensive parsing against `exc.details` because Gemini error payloads may vary. The implementation should look for:
|
| 78 |
+
|
| 79 |
+
- `RetryInfo` details with `retryDelay`
|
| 80 |
+
- `QuotaFailure` details with a `quotaId`
|
| 81 |
+
- quota identifiers containing daily-scope markers such as `PerDay`
|
| 82 |
+
|
| 83 |
+
If `retryDelay` is greater than 600 seconds, or if a quota identifier indicates a daily/RPD limit, raise a batch-abort signal instead of applying the 60/120/300 second lane cooldown. Waiting 300 seconds does not fix `50K/day` exhaustion on `gemini-3.1-pro`.
|
| 84 |
+
|
| 85 |
+
If metadata is absent or unparseable, fall back to the normal lane cooldown ladder.
|
| 86 |
|
| 87 |
### In-process cooldown registry
|
| 88 |
|
|
|
|
| 93 |
(provider, model) -> consecutive_rate_limit_exhaustions
|
| 94 |
```
|
| 95 |
|
| 96 |
+
Use `time.monotonic()` for all cooldown calculations.
|
| 97 |
+
|
| 98 |
Before invoking the wrapped model, `_RetryWrapper` checks whether its lane has an active cooldown. If active, it sleeps until the cooldown expires, then proceeds with the existing tenacity retry loop.
|
| 99 |
|
| 100 |
After a successful call, the lane's consecutive rate-limit exhaustion count resets to zero.
|
| 101 |
|
| 102 |
+
Cooldown state is shared across `_RetryWrapper` instances for the same lane and isolated across different lanes. A cooldown on `google:gemini-3.1-pro` must not delay a `google:gemini-3-flash-preview` call.
|
| 103 |
|
| 104 |
+
### Cooldown trigger and duration
|
| 105 |
|
| 106 |
+
The cooldown layer sits on top of the existing tenacity retry ladder. A normal exhausted call still spends time inside tenacity first. With the current retry settings, the rough pre-cooldown wait sequence is approximately:
|
| 107 |
|
| 108 |
+
```text
|
| 109 |
+
4s -> 8s -> 16s -> 32s -> 60s
|
| 110 |
+
```
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
Only after tenacity exhausts retryable 429 attempts should the wrapper record lane exhaustion and raise a rate-limit signal.
|
| 113 |
|
| 114 |
+
Use this conservative escalating cooldown for repeated failures on the same lane:
|
| 115 |
|
| 116 |
| Consecutive exhausted rate limits | Cooldown |
|
| 117 |
| ---: | ---: |
|
|
|
|
| 119 |
| 2 | 120 seconds |
|
| 120 |
| 3+ | 300 seconds |
|
| 121 |
|
| 122 |
+
If Gemini supplies a shorter explicit retry delay for non-daily quota pressure, a future patch can prefer it. This patch only needs the daily-vs-short-cooldown distinction.
|
| 123 |
+
|
| 124 |
+
### Rate-limit signals
|
| 125 |
+
|
| 126 |
+
Introduce project-level exceptions:
|
| 127 |
+
|
| 128 |
+
- `RateLimitCooldownError`
|
| 129 |
+
- Raised when one model invocation exhausts tenacity retries on a retryable, non-daily rate limit.
|
| 130 |
+
- Carries provider, model, cooldown seconds, and original exception text.
|
| 131 |
+
|
| 132 |
+
- `QuestionRateLimitStreakError`
|
| 133 |
+
- Raised when the current GAIA question reaches the per-question 429 streak threshold.
|
| 134 |
+
- Used to skip the current question without burning more model calls.
|
| 135 |
+
|
| 136 |
+
- `BatchAbortRateLimitError`
|
| 137 |
+
- Raised when metadata indicates daily/RPD exhaustion or a retry delay longer than 600 seconds.
|
| 138 |
+
- Used to stop the batch rather than polling through every remaining task.
|
| 139 |
+
|
| 140 |
+
Non-rate-limit exceptions should keep their existing behavior and should not be rewritten as cooldown errors.
|
| 141 |
+
|
| 142 |
+
### Per-question streak threshold
|
| 143 |
+
|
| 144 |
+
The runner should execute each task inside a rate-limit question scope. Within that scope, `_RetryWrapper` records each observed retryable 429 event for the current question.
|
| 145 |
+
|
| 146 |
+
If one question accumulates 50 consecutive retryable 429 events, abort that question with `QuestionRateLimitStreakError`.
|
| 147 |
+
|
| 148 |
+
The streak resets when:
|
| 149 |
+
|
| 150 |
+
- the current question finishes,
|
| 151 |
+
- a model call succeeds,
|
| 152 |
+
- a non-rate-limit error occurs,
|
| 153 |
+
- the runner moves to the next GAIA task.
|
| 154 |
+
|
| 155 |
+
This protects against a single hard question or loop burning retry ladders indefinitely.
|
| 156 |
+
|
| 157 |
+
### Cross-task sliding window
|
| 158 |
+
|
| 159 |
+
Maintain an in-process sliding window over recent model-call outcomes during a GAIA batch. One outcome means one lower-level model request attempt observed by the retry wrapper, including tenacity retry attempts; it does not mean one GAIA task.
|
| 160 |
+
|
| 161 |
+
```text
|
| 162 |
+
window size: 100 model-call outcomes
|
| 163 |
+
pause condition: >= 70 rate-limited outcomes in the window
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
When the pause condition is met, the runner should pause the whole batch with exponential backoff:
|
| 167 |
+
|
| 168 |
+
| Consecutive batch pauses | Pause |
|
| 169 |
+
| ---: | ---: |
|
| 170 |
+
| 1 | 300 seconds |
|
| 171 |
+
| 2 | 600 seconds |
|
| 172 |
+
| 3+ | 1200 seconds |
|
| 173 |
+
|
| 174 |
+
After a batch pause, clear the sliding window so the same old failures do not immediately trigger another pause.
|
| 175 |
+
|
| 176 |
+
This is separate from lane cooldown. Lane cooldown handles the next call on a specific model; the sliding window handles the case where many tasks across the batch are hitting the same quota wall.
|
| 177 |
|
| 178 |
### Runner behavior
|
| 179 |
|
| 180 |
+
`runner.py::run_agent_on_questions` is currently sequential. This design assumes sequential batch execution and uses synchronous sleeps. If the runner later becomes concurrent, cooldown and pause sleeps must be revisited.
|
| 181 |
|
| 182 |
For each GAIA task:
|
| 183 |
|
| 184 |
+
1. Invoke the graph normally inside the existing `with ephemeral_memory():` task isolation block.
|
| 185 |
2. If a `RateLimitCooldownError` escapes:
|
| 186 |
- log the task, model, and cooldown duration,
|
| 187 |
- sleep for the indicated cooldown,
|
| 188 |
+
- retry the same task once inside a new `with ephemeral_memory():` block.
|
| 189 |
3. If the retry succeeds, format and checkpoint the answer normally.
|
| 190 |
4. If the retry still fails with rate-limit exhaustion, append an `AGENT ERROR: RATE LIMITED` answer for this run and continue to the next question.
|
| 191 |
+
5. If `QuestionRateLimitStreakError` escapes, append an answer beginning with `AGENT ERROR: RATE LIMITED` and continue to the next question.
|
| 192 |
+
6. If `BatchAbortRateLimitError` escapes:
|
| 193 |
+
- do not write a normal success checkpoint for the current task,
|
| 194 |
+
- append an in-memory answer beginning with `AGENT ERROR: RATE LIMITED`,
|
| 195 |
+
- write a non-success diagnostic marker at `<checkpoint_dir>/rate_limit_abort.json`,
|
| 196 |
+
- stop the batch and return answers collected so far.
|
| 197 |
|
| 198 |
+
The `AGENT ERROR:` prefix matches the existing runner convention and ensures any shared checkpoint guard continues to treat the value as non-success.
|
| 199 |
|
| 200 |
## Data Flow
|
| 201 |
|
| 202 |
```text
|
| 203 |
GAIA task
|
| 204 |
+
-> runner opens ephemeral_memory()
|
| 205 |
-> runner invokes graph
|
| 206 |
-> model node calls Gemini lane
|
| 207 |
-> _RetryWrapper checks lane cooldown
|
| 208 |
+
-> tenacity retries using is_retryable_rate_limit
|
| 209 |
+
-> each observed 429 updates question scope + batch window
|
| 210 |
+
-> exhausted non-daily 429 records lane cooldown
|
| 211 |
-> RateLimitCooldownError bubbles up
|
| 212 |
-> runner waits cooldown
|
| 213 |
+
-> runner retries same GAIA task once in a fresh ephemeral_memory()
|
| 214 |
-> success checkpoints answer OR failure continues without success checkpoint
|
| 215 |
```
|
| 216 |
|
| 217 |
+
For daily/RPD exhaustion:
|
| 218 |
+
|
| 219 |
+
```text
|
| 220 |
+
Gemini 429 payload indicates PerDay or retryDelay > 600s
|
| 221 |
+
-> BatchAbortRateLimitError
|
| 222 |
+
-> runner writes diagnostic marker
|
| 223 |
+
-> runner stops batch
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
## Error Handling
|
| 227 |
|
| 228 |
+
- Rate-limit exhaustion should be logged at warning level with provider, model, cooldown, and task id when available.
|
| 229 |
- Generic graph/model/tool errors should continue through the existing `AGENT ERROR` path.
|
| 230 |
- A failed retry after cooldown should not crash the whole batch.
|
| 231 |
- A successful call after cooldown should reset that model lane's consecutive rate-limit count.
|
| 232 |
+
- Non-429 `google.genai.errors.ClientError` must not trigger retry, cooldown, question streak, or batch pause.
|
| 233 |
+
- Process restart resets in-process cooldown, streak, and sliding-window state by design.
|
| 234 |
|
| 235 |
## Testing Plan
|
| 236 |
|
| 237 |
Add focused unit tests without hitting external APIs:
|
| 238 |
|
| 239 |
+
1. `is_retryable_rate_limit` returns true for `google.genai.errors.ClientError` with code `429`.
|
| 240 |
+
2. `is_retryable_rate_limit` returns false for `google.genai.errors.ClientError` with code `400`.
|
| 241 |
+
3. `_RetryWrapper` sleeps when a lane cooldown is active.
|
| 242 |
+
4. `_RetryWrapper` records a 60-second cooldown after first exhausted retryable error.
|
| 243 |
+
5. Consecutive exhausted rate-limit errors escalate to 120 seconds and then 300 seconds.
|
| 244 |
+
6. Successful calls reset the consecutive lane failure counter.
|
| 245 |
+
7. Two `_RetryWrapper` instances on the same lane share cooldown.
|
| 246 |
+
8. Cooldown on `gemini-3.1-pro` does not delay `gemini-3-flash-preview`.
|
| 247 |
+
9. Cooldown uses `time.monotonic()` rather than wall-clock time.
|
| 248 |
+
10. Daily quota metadata or `retryDelay > 600s` raises `BatchAbortRateLimitError`.
|
| 249 |
+
11. `run_agent_on_questions` retries the same task once after `RateLimitCooldownError`.
|
| 250 |
+
12. `run_agent_on_questions` opens a fresh `ephemeral_memory()` block for the retry.
|
| 251 |
+
13. `run_agent_on_questions` does not write a normal success checkpoint when both attempts are rate-limited.
|
| 252 |
+
14. A 50-event per-question 429 streak skips the current question.
|
| 253 |
+
15. A 100-outcome sliding window with at least 70 rate-limited outcomes triggers a batch pause.
|
| 254 |
+
16. Unknown/non-Google models do not receive Gemini-specific cooldown profiles.
|
| 255 |
+
|
| 256 |
+
Tests should monkeypatch sleep/time functions so the suite does not actually wait.
|
| 257 |
|
| 258 |
## Risks and Mitigations
|
| 259 |
|
| 260 |
- **Risk: sleeping in tests slows the suite.**
|
| 261 |
- Mitigation: inject or monkeypatch sleep/time functions in tests.
|
| 262 |
|
| 263 |
+
- **Risk: synchronous sleep blocks future concurrent runners.**
|
| 264 |
+
- Mitigation: document sequential runner assumption; current `run_agent_on_questions` loops tasks sequentially.
|
| 265 |
+
|
| 266 |
- **Risk: cooldown is only in-process.**
|
| 267 |
+
- Mitigation: acceptable for current local batch runs; process restart resetting cooldown is a deliberate non-goal.
|
| 268 |
|
| 269 |
- **Risk: same-question retry duplicates some work.**
|
| 270 |
- Mitigation: retry only once and use a fresh ephemeral memory store, matching existing per-task isolation.
|
| 271 |
|
| 272 |
+
- **Risk: Gemini error payloads vary.**
|
| 273 |
+
- Mitigation: classify 429s by `exc.code` first; parse quota metadata defensively; fall back to short cooldown only when daily quota cannot be detected.
|
| 274 |
|
| 275 |
## Implementation Boundaries
|
| 276 |
|