fix(llm): fallback chain walk honors namespace-qualified model ids - #8896
fix(llm): fallback chain walk honors namespace-qualified model ids#8896javenciu wants to merge 2 commits into
Conversation
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS Closes the last literal-membership consumer through the existing shared fold, keeps chain vs. wire spellings from diverging, and opens no one-way door. [DESIGN-REVIEWED] 7582844 |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of All claims verified against the base tree. I have what I need: the fold exists with five prior consumers, the restore-probe comparison at First-Principles-Verdict: PASS A real, silently-failing defect fixed at cause level with the existing shared fold; every item traces to the fix, one helper could shrink. What this change shipsIntent: make a persisted namespace-qualified fallback entry actually fire when throttling hits, instead of being silently skipped. FIX.
The fix reuses Subtractions
[FIRST-PRINCIPLES-REVIEWED] 7582844 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. The single candidate does not survive falsification. Its scenario requires the primary/active to be stored in the qualified
Identical outcome, so the diff introduces no regression here — the un-folded comparison target is pre-existing behavior the PR neither created nor worsened (the candidate itself concedes this and rates it low). Separately, (a) is not grounded: the served/active model read from No new grounded defect found: the walk returns the chain's own spelling so [OPUS-REVIEWED] 7582844 |
|
Re: Opus 4.8 suggestion — import Commit 1 (0a830e7) had exactly that shape and failed Per Happy to reroute if maintainers prefer a different shape, but as the gate stands, the suggested import is the one it rejects. |
|
Backend Tests (3.12, 4) failure — the #8893 flake, third occurrence, not this diff
Root cause is confirmed at source (details in #8893): both ordering tests patch No changes to this PR; requesting a rerun of the failed shard once maintainers deem it appropriate. |
5c645c8 to
35f0e8d
Compare
|
CI note: the |
…rodotdev#8740) The throttle-fallback walk filtered persisted agent.fallback_model chain entries by literal membership in the advertised set, so a chain entry carrying a stale <namespace>::<bare-id> qualifier was skipped as unadvertised even when the backend serves the bare id, and the walk fell through toward "auto"/exhaustion. next_fallback_candidate now judges membership and the active-model skip through resolve_pin_spelling (the shared fold from kirodotdev#8616/kirodotdev#8737); an entry absent under both spellings is still skipped. The walk returns the chain's own spelling (FallbackState.next_candidate locates it with remaining.index), while advance_fallback_candidate sends and records the advertised spelling everywhere a served model is later compared: the substitute set_model call, the silent no-op witness, the TURN_FALLBACK_ATTR marker the restore probe reads, fb_state.active and walked, and the returned candidate. fallback_wire_spelling is the one home of that translation. Doc: model-fallback.md candidate-walk section synced.
The agent-sdk-boundary gate refuses a new ACP-layer import edge on an added line even when the module holds baselined edges: commit 1 folded resolve_pin_spelling into the existing kiro_crew.acp.client import, which both added the edge and rewrote the baselined line. Restore that import line byte-identical to the baseline and take resolve_pin_spelling from kiro_crew.agent_sdk.drivers.acp instead -- the delegation that exists for exactly this gate (same shape as session.py and dashboard/handlers/agents.py). Plain data in, plain data out; no behavior change.
35f0e8d to
7582844
Compare
|
The
This is the 4th occurrence of the #8893 signature across recent CI runs on unrelated PRs. No code change is needed here; happy to have the shard re-run whenever convenient. |
Problem / Motivation
A persisted
agent.fallback_modelchain entry that carries a<namespace>::<bare-id>qualifier (the #8521 mismatch class — stored when acatalog advertised the qualified spelling) is silently skipped by the
throttle-fallback walk when the session advertises the bare id.
next_fallback_candidate(src/kiro_crew/llm_helpers.py) filters the chainby literal membership:
so a chain entry the backend fully serves is treated as unadvertised, and the
walk falls through toward
"auto"/chain exhaustion. On a partition that doesnot serve
"auto", the chain exhausts and the original throttle errorsurfaces — the configured fallback never fires. Issue #8740 identified this as
the one literal-membership site left outside the #8616/#8737 fold.
Why it matters
Users who pinned a fallback while a catalog advertised qualified spellings get
no fallback at all at exactly the moment it exists for: sustained throttling
on the primary. The failure is silent (a debug-level skip log), so it reads as
"fallback feature does not work". The same stale-qualifier pins were already
judged worth folding at the substitute-send and picker sites (#8616, #8737);
the fallback walk was the remaining consumer of persisted model values still
comparing literally.
What changed (motivation → approach → change)
Observed symptom: a served-but-qualified chain entry is skipped as
unadvertised (reproducer below — the walk returns
Noneon a chain whose onlyentry the backend serves under its bare spelling).
Root cause:
next_fallback_candidatejudges membership with a literal setlookup, and the issue's own analysis explains why the fold could not be a
one-line predicate swap: the walk's return value feeds two consumers with
conflicting spelling needs.
FallbackState.next_candidatelocates the appliedcandidate with
remaining.index(cand), so it needs the CHAIN's own spelling —but the wire and every later served-model comparison (the
AcpClient.set_modelexplicit-pick guard, the silent no-op witness, theTURN_FALLBACK_ATTRmarker thatprobe_fallback_restorecompares against theserved model, and
fb_state.active/walked) need the ADVERTISED spelling. Aqualified spelling in the marker would make the restore probe read the session
as having moved off the fallback, clear the sticky state, and skip the slot
heal.
The change, keeping the two spellings from ever disagreeing:
next_fallback_candidatejudges membership and the active-model skipthrough
resolve_pin_spelling(the shared fold from Namespace-qualified model ids: sibling comparison sites not covered by #8615 #8616/fix(models): fold namespaced pins at substitute and picker sites (#8616) #8737): full idfirst, one leading
<namespace>::peel on a miss. An entry absent underboth spellings is still skipped (deny-parity), a verbatim-advertised
qualified id is never peeled, and a qualified entry that resolves to the
currently-failing model is skipped post-fold. The function still returns the
chain's own spelling, preserving the
remaining.indexbookkeeping contractand all ten existing behavior pins.
fallback_wire_spellingis the single home of the chain→wiretranslation:
advance_fallback_candidatecomputes it once per candidate anduses it for the in-loop active skip, the
set_modelcall, the no-opwitness,
fb_state.active/walked, the published marker, the swap log, andthe return value. Empty/unknown advertised set falls back to the entry's own
spelling, matching the walk's existing fail-open stance.
docs/system-specs/features/model-fallback.mdcandidate-walk section syncedin the same commit.
Alternative rejected: returning a
(chain_entry, wire_spelling)tuple fromnext_fallback_candidate. It changes the function's contract and everyexisting pin for the same information the shared fold can recompute
deterministically at the one wire consumer; the smaller surface keeps the fold
authoritative in one place.
Tests
Reproducer, before the fix (tests-only tree at base
235d36a62):After the fix:
6 passed, and the full targeted file142 passed.New pins and what each locks in:
test_namespace_qualified_entry_folds_to_advertised_bare_id— a qualifiedentry the backend serves under its bare id is selected, and the CHAIN
spelling is returned (bookkeeping contract).
test_qualified_entry_peeling_to_active_model_is_skipped— post-fold activeskip: a qualified entry resolving to the failing model cannot help.
test_entry_absent_under_both_spellings_still_skipped— deny-parity: thefold does not weaken the advertised filter.
test_verbatim_advertised_qualified_id_not_peeled— a full-id match wins;peeling never rewrites it.
TestAdvanceFallbackCandidateNamespacedChain::test_qualified_entry_applies_under_advertised_spelling— end to end:
set_modelawaited with the bare spelling, marker(primary, bare),fb_state.active/walkedcarry the bare spelling, andposadvanced past the chain entry itself.TestAdvanceFallbackCandidateNamespacedChain::test_qualified_entry_peeling_to_failing_model_is_skipped— the walk exhausts rather than announcing a no-op swap when the chain names
the failing model under a qualifier.
Neighboring suites green locally:
test_subagent_turn_resilience.py+test_model_selection_scenarios.py+test_api_models_entitlement.py(84 passed),
test_dashboard_chat.py -k fallback(16 passed). mypy, flake8,isort, the black baseline gate, and
docs-lint.shall pass on the changedfiles.
Manual verification
N/A — unit coverage sufficient: the walk, the wire call, the witness, and the
marker are all exercised through the same provider-double shape the existing
fallback tests use, and the seam has no UI or external-service surface.
Related Issues
Fixes #8740
Pattern harvest
Rule candidate: review-prompt
Pattern: "persisted model ids must be compared through the shared spelling
fold, never by literal membership; any value later compared against a SERVED
model must carry the advertised spelling". #8616 → #8737 → this PR closed the
enumeration one site at a time; a review prompt on new
in advertised-stylemembership tests over persisted model values stops the class from re-growing.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement
Per the template placeholder (CLA text pending): offered under the same terms as my prior merged contributions to this repository (#8835).