Skip to content

refactor(opencode): name the retry cap for its cumulative scope - #141

Merged
Alezander9 merged 1 commit into
mainfrom
retry-budget-naming
Aug 3, 2026
Merged

refactor(opencode): name the retry cap for its cumulative scope#141
Alezander9 merged 1 commit into
mainfrom
retry-budget-naming

Conversation

@Alezander9

@Alezander9 Alezander9 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Follow-up to #137. Naming and documentation only — no behavior change.

The mismatch

maxAttempts lives on the per-error descriptor returned by retryable(), so it reads as "attempts allowed for this error". It is actually compared against Schedule.InputMetadata.attempt, which metadataFn implements as a single ++n per schedule instance (effect/dist/Schedule.js:109-127) — one counter per request, incremented on every step regardless of which reason produced it.

So the field is a total, not a per-reason budget. Renamed to maxTotalAttempts and documented at both sites.

Why it matters

processor.ts:733 has one Effect.retry wrapping the whole stream drain, so two reasons share that counter:

  • rate limit / overload, surfaced from llm.stream — no cap, retries indefinitely with backoff
  • finishReason === "length" thrown at processor.ts:524 — maxTotalAttempts: 3

429, 429, finishReason=length reaches the output-limit branch at attempt=3, so 3 >= 3 gives it zero resamples and the turn fails with "Model hit its output limit". The sharing is one-directional: rate limits set no cap, so they never lose budget to a prior output-limit attempt.

The homogeneous case — the one #137 was designed around — is unaffected and stays at 2 retries / 3 calls, byte-identical to the counter it replaced (++outputLengthRetries > 2 also retried errors 1 and 2 and gave up on 3).

Which behavior is right

I did not change it. Bounding total work on a request that has already failed three times is defensible, and the consequence when it fires is one lost resample on a request where the provider was already misbehaving — no data loss, and it is cheaper rather than more expensive. The problem was that the name claimed per-reason scope while the code did cumulative, and no test pinned either reading.

If independent per-reason budgets are actually wanted, that needs a Map<reason, count> closure in policy() — which is the mutable state #137 deliberately deleted, so it should be a conscious choice rather than a silent restoration. Happy to do that instead if you prefer.

Tests

Adds the mixed-reason case the suite was missing, so the cumulative semantics are now pinned either way. It fails against a per-reason implementation, which is the point.

bun test test/session/retry.test.ts → 35 pass. bun typecheck clean.

Unrelated, noticed while verifying: test/session/processor-effect.test.ts is 1 pass / 15 fail on origin/main with Model not found: test/test-model, identical before and after this branch. The only CI workflow is typecheck, so nothing runs the suite on main. Flagging separately, not touching it here.


Summary by cubic

Renamed the retry cap to maxTotalAttempts to match the shared, cumulative attempt counter across a request. Added inline docs and a mixed‑reason test; no behavior change.

  • Refactors

    • Renamed maxAttempts to maxTotalAttempts in retryable() and policy().
    • Documented that the cap is cumulative across reasons and measured against the per-request meta.attempt.
  • Tests

    • Added a case proving earlier rate-limit retries consume the budget for a later output-limit error.

Written for commit 76a7a6c. Summary will update on new commits.

Review in cubic

maxAttempts sits on the per-reason Retryable descriptor but is compared
against Schedule's per-request attempt counter, which every retry reason
shares. The name reads as a per-reason budget, so rename it to
maxTotalAttempts and document the sharing at both sites.

No behavior change. Adds the mixed-reason test the suite was missing:
two rate-limit retries leave the output-limit path no resample.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

@Alezander9
Alezander9 merged commit 6b3408f into main Aug 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant