fix(jiradozer): model-aware plan-limit pre-flight + drop incompatible fallback effort#281
Merged
Conversation
… fallback effort Four runs (INF-1906/1907/1908/1912) died terminally on "provider does not support reasoning effort: provider=cursor level=high" after the plan-limit pre-flight (#277) skipped opus -> composer-2.5. Two independent bugs, both confirmed against the live /api/oauth/usage payload. Bug 1 — false limit detection (model-blind). The payload had opus's session window at 20% but a Fable-scoped weekly cap at 100% (is_active, scope.model = "Fable"). MaxActiveUtilization() collapsed all buckets into one scalar and ignored scope.model, so the Fable cap tripped the skip for opus. - Parse `scope` in PlanLimit; add MaxActiveUtilizationForModel(id,label) that counts unscoped buckets plus scoped buckets matching the target model (case-insensitive on id/display_name). ClaudeSessionUtilization now calls it. - Keep model-blind MaxActiveUtilization() for account-wide reporting. Bug 2 — incompatible effort carried to fallback. runAgent swapped only the model on fallback and kept the inherited effort=high; cursor/gemini/agy have no effort knob and hard-fail, so the rescue fallback was guaranteed to die. - Add agent.ProviderSupportsEffort (claude/codex yes; cursor/gemini/agy no). - Blank activeCfg.Effort for any fallback model whose provider can't honor it; codex fallbacks keep the configured effort. Also add agent.disable_limit_preflight (default false) to turn off the proactive pre-flight entirely and rely only on reactive out-of-credits fallback. Threaded through ResolveStep/ResolveRound; documented in the bootstrap agent block and regenerated jiradozer.example.yaml. Tests: model-aware utilization (incl. verbatim incident payload), ProviderSupportsEffort, fallback effort drop/keep, and pre-flight disable.
Cleanup pass over the model-aware pre-flight change: extract a named ScopedModel type (was an anonymous struct repeated verbatim in a test), trim doc/test comments that narrated the task/incident down to load-bearing WHY, de-duplicate the DisableLimitPreflight doc to one canonical spot, and share a claudeAtLimit test helper across the fallback tests.
…drop Address two codex review findings: - MaxActiveUtilizationForModel was model-aware only for the limits[] schema; the legacy named-window fallback still lumped SevenDayOpus/SevenDaySonnet (model-scoped caps) into every model's max, so Opus could be skipped because Sonnet was capped. Gate the named model-scoped windows by their own model. - The fallback effort-drop also fired for a directly-configured primary, silently downgrading an explicit effort that a no-knob provider should reject (per the --thinking-level contract). Restrict the drop to failover (mi > 0).
Codex flagged that the model-aware usage matcher compared plan scopes against
exact resolved model strings, so a versioned config (claude-opus-4-8) would
bypass a scoped cap the API reports under the bare family ("Opus") — in both
the limits[] path and the legacy SevenDayOpus/SevenDaySonnet fallback.
scopeIDMatches now also compares by Claude family (opus/sonnet/haiku/fable) via
a new claudeModelFamily helper, so versioned ids, bare aliases, and API display
names all resolve to the same family. Single-point fix at the matcher covers
every cited call site; added versioned-id regression cases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Four jiradozer runs on 2026-07-08 (INF-1906/1907/1908/1912) died terminally with:
The proactive plan-limit pre-flight (#277) fired, skipped
opus → composer-2.5, and the fallback immediately hard-failed. Two independent root causes, both confirmed against the live/api/oauth/usagepayload.Root cause 1 — false limit detection (model-blind)
The live payload had the
opussession window at only 20%, but a Fable-scoped weekly cap at 100% (is_active: true,scope.model.display_name: "Fable").PlanUsage.MaxActiveUtilization()collapsed every active bucket into one scalar and ignoredscope.model, so a Fable-only cap tripped the pre-flight skip for Opus, which still had plenty of headroom.Fix: parse
scopeinPlanLimitand addMaxActiveUtilizationForModel(modelID, modelLabel)— it counts unscoped buckets plus scoped buckets that match the target model (case-insensitive on id/display-name), and excludes scoped buckets belonging to a different model.ClaudeSessionUtilizationnow uses the model-aware variant. The model-blindMaxActiveUtilization()is retained for account-wide reporting.Root cause 2 — incompatible effort carried to fallback
On fallback,
runAgentswapped only the model (activeCfg.Model = modelID) and kept the inheritedeffort: high. Cursor/Gemini/Agy have no effort knob and hard-fail on any non-auto level, so the rescue fallback was structurally guaranteed to die.Fix: added
agent.ProviderSupportsEffort(provider)(Claude/Codex → yes; Cursor/Gemini/Agy → no) and, in the fallback loop, blankactiveCfg.Effortfor any model whose provider can't honor it. Codex fallbacks still keep the configured effort (negative-control test).Extra — opt-out switch
Added
agent.disable_limit_preflight(defaultfalse) to turn off the proactive pre-flight entirely and rely only on the reactive out-of-credits fallback. Threaded throughResolveStep/ResolveRound, documented in the bootstrap agent block, andjiradozer.example.yamlregenerated.Tests
MaxActiveUtilizationForModel— table-driven, including the verbatim incident payload (session 20% inactive + Fable weekly cap 100% active): Opus not gated, Fable gated at 100%, model-blind view unchanged.ProviderSupportsEffort— all five providers + unknown.disable_limit_preflightskips the pre-flight even at 100% and never queries utilization.Verification
bazel test //...→ 92/92 passscripts/lint.sh→ all checks pass/api/oauth/usagepayload end-to-end through the realPlanUsageJSON decoding.