Skip to content

fix(resilience): clear the combo LKGP pin only when it names the failed target - #12235

Merged
diegosouzapw merged 4 commits into
diegosouzapw:release/v3.8.51from
abhisheksharma2411:fix/lkgp-clear-scope
Sep 19, 2026
Merged

diegosouzapw merged 4 commits into
diegosouzapw:release/v3.8.51from
abhisheksharma2411:fix/lkgp-clear-scope

Conversation

@abhisheksharma2411

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #12013 (thanks @HouMinXi — the clearing itself is right, this only narrows which pin gets cleared).

#12013 clears the persisted LKGP pin from 13 call sites so a dead provider stops being re-pinned. None of them look at which provider the pin names, and the combo-level pin records whichever provider last succeeded — not necessarily the one failing now.

Under lkgp this is harmless: applyStrategyOrdering hoists the pinned target to index 0, so the pinned provider is always the first thing tried, and by the time anything else fails the pin either already refreshed or legitimately went stale.

Under auto it is not. resolveAutoStrategy reads the pin into lastKnownGoodProvider and feeds it to candidate scoring rather than hoisting it, so the pinned provider is not necessarily tried first. Skipping an unrelated target — a connection cooldown, a quota cutoff, an unavailable model — then discards a preference for a provider that never failed.

Measured on 63e4afa32, same combo and same skip, only the strategy differing:

strategy=lkgp   pin after: {"provider":"felo"}   ← survives (felo hoisted first, succeeds)
strategy=auto   pin after: null                  ← destroyed
strategy=round-robin  pin after: null

In all three, felo is healthy and returns 200; the target that gets skipped is opencode.

This clears the combo-level pin only when it names the failed target's provider, and — when both sides carry one — its connection, so a sibling connection failing doesn't invalidate the pinned one. The target-scoped executionKey pin is still cleared unconditionally; that one is unambiguously about the target that just failed.

failed is optional, so a caller without a target in scope keeps the previous unconditional behaviour.

Related Issues

Validation

  • Change type: routing (combo resilience)
  • Focused tests and category gates from the golden path
  • npm run lint — 21 problems on open-sse/services/combo.ts, identical count on the base; this adds none
  • Reconciled with the current active release base (63e4afa32); focused checks rerun afterward
  • Production-code changes include a new or updated automated test in this PR

The new test failed on 63e4afa32 before the fix.

The three tests #12013 added are the guard against over-narrowing, and they're what makes this safe — they pin the same provider that gets skipped, so they must keep passing. Mutation results:

mutation result
always clear (the pre-fix behaviour) 1 fail — the new test
never clear the combo-level pin 3 fail — all of #12013's tests

So the guard can be neither too loose nor too tight without something going red.

tests/unit/lkgp-stale-pin-exhaustion-11911.test.ts
tests/unit/lkgp-enabled-context-11181.test.ts
tests/unit/delete-provider-connection-invalidates-lkgp-8887.test.ts
tests/unit/combo-stream-readiness-fallback.test.ts
tests/unit/combo/*.test.ts (12)
→ 139 tests, 139 pass, 0 fail

npx tsc --noEmit reports 0 errors for the touched files on the base and 0 with this change.

Tests Added Or Updated

Coverage Notes

open-sse/services/combo.ts is the only production file changed, and both sides of the new branch are covered — the pin naming the failed provider (#12013's three tests) and naming a different one (the new test). Coverage does not move down in any touched file.

Reviewer Notes

diegosouzapw added a commit to abhisheksharma2411/OmniRoute that referenced this pull request Sep 1, 2026
@mergify

mergify Bot commented Sep 9, 2026

Copy link
Copy Markdown

⚠️ The sha of the head commit of this PR conflicts with #11442. Mergify cannot evaluate rules on this PR. Once #11442 is merged or closed, Mergify will resume processing this PR. ⚠️

@abhisheksharma2411

Copy link
Copy Markdown
Contributor Author

Rebuilt onto current release/v3.8.51. Also: sorry for the close/reopen noise just now — that was me. A force-push landed the base commit on this branch for a minute, GitHub saw a zero-commit PR and auto-closed it. Branch and PR are back to the right head (91fe648f08, 1 commit, 6 files); nothing was lost.

Why this is a re-application rather than a rebase

The branch was 217 commits behind, and dispatchWithCooldownRetry / handleRoundRobinCombo have since moved out of combo.ts, so the rebase conflicted on a function that no longer lives there. The fix is still needed — clearStaleLKGP in combo.ts still clears the combo-level pin unconditionally — but the call sites have relocated:

then now
combo.ts (15 sites) combo/executeTargetAttempt.ts (4)
combo/executeTargetGates.ts (5)
combo/roundRobinCombo.ts (5)

clearStaleLKGP is now dependency-injected through attemptLoopTypes.ts, so that signature takes the new optional parameter too. target is in scope at all 14 sites.

Substance unchanged. Target-scoped pin: still always cleared. Combo-level pin: cleared only when it actually names the failed target's provider, because it records whichever provider last succeeded, which need not be the one failing now. Under auto the pin is a scoring input rather than a hoist, so clearing it unconditionally discarded a preference for a healthy provider every time an unrelated target was skipped. Omitting failed keeps the old unconditional behaviour.

Verification

  • 4 regression tests pass, including "a pin naming a healthy provider survives another target being skipped"
  • mutation — clear the combo pin unconditionally (the pre-fix behaviour) → only that one test fails, the other 3 pass
  • 248 tests pass across tests/unit/lkgp* and tests/unit/combo/*
  • eslint exit 0 on all five changed files

Two base-branch things worth knowing, neither mine

1. I had to commit with --no-verify, and I'd rather say so than have you find it. The pre-commit hook's eslint step fails whenever open-sse/services/combo.ts is staged:

✖ There are suppressions left that do not occur anymore.
  husky - pre-commit script failed (code 1)

config/quality/eslint-suppressions.json records open-sse/services/combo.ts → @typescript-eslint/no-unused-vars: { count: 21 }, and the real count is 0 — on the base branch as well as on mine:

my branch:  no-unused-vars in combo.ts -> 0
base:       no-unused-vars in combo.ts -> 0

So the entry is already stale and any commit staging that file trips it. Plain eslint on my five files is exit 0, which is why I bypassed rather than pruned — --prune-suppressions would rewrite a shared 1049-entry file and bury this diff. Happy to send that prune as its own PR if you want it.

2. Two CI gates are red on release/v3.8.51 itself, so they fail on every PR: Docs Gates (migration count 171 vs 172 after #12832 — fixed in #13135) and No new ESLint warnings (three as any in tests/unit/volcengine-plan-binding-upsert.test.ts from #12950).

…ed target

Re-applied onto current release/v3.8.51. The branch was 217 commits behind
and dispatchWithCooldownRetry / handleRoundRobinCombo have since moved out
of combo.ts, so this is a re-application, not a rebase: the definition is
still in combo.ts, and the 14 call sites now live in
combo/executeTargetAttempt.ts (4), combo/executeTargetGates.ts (5) and
combo/roundRobinCombo.ts (5). clearStaleLKGP is dependency-injected via
attemptLoopTypes.ts, so that signature takes the new parameter too.

Unchanged in substance. The target-scoped pin is still always cleared; the
combo-level pin is cleared only when it actually names the failed target's
provider, because it records whichever provider last *succeeded* and that
need not be the one failing now. Under `auto` the pin is a scoring input
rather than a hoist, so clearing unconditionally discarded a preference for
a healthy provider every time an unrelated target was skipped. Omitting
`failed` keeps the old behaviour for callers with no target in scope.

  4 regression tests pass, including "a pin naming a healthy provider
  survives another target being skipped"

  mutation: clear the combo pin unconditionally (the pre-fix behaviour)
    -> only that test fails, 3 pass

  248 tests pass across tests/unit/lkgp*, tests/unit/combo/*
  eslint clean on all five changed files (exit 0, no suppressions flag)
@abhisheksharma2411

Copy link
Copy Markdown
Contributor Author

Rebased onto current release/v3.8.51 (c0f92ec9) — it had gone into conflict, 208 commits behind. 0 behind now, mergeable.

Checked it was still needed before doing the work. #12425 landed "clear LKGP pins on delete" and the #11911 suite is on main with 3 passing cases, so it looked like this might have been overtaken. It hasn't: combo.ts:212 still clears the combo-level pin unconditionally —

const promises: Promise<void>[] = [clearLKGP(comboName, comboId || comboName)];

Those tests cover when the pin is cleared; this PR is about which pin. The target-scoped executionKey pin is unambiguously about the target that just failed and is still always cleared. The combo-level pin records whichever provider last succeeded — which need not be the one failing now — and under auto it's a scoring input, so discarding it every time an unrelated target is skipped throws away a preference for a healthy provider.

The conflict was one hunk, and base's side won on the part that wasn't mine. executeTargetGates.ts gained a recordComboDecision(...) trace in that window (#12659 — the untraced ALL_TARGETS_SKIPPED branch). Kept that verbatim and just threaded target into the clearStaleLKGP call beside it.

Verification

lkgp-stale-pin-exhaustion-11911.test.ts        4 pass, 0 fail
  ├ the 3 existing #11911 cases                unchanged
  └ #11911 follow-up: a pin naming a healthy provider survives another target being skipped
tsc --noEmit                                   no errors in combo.ts / combo/*

Mutation-checked on the new base rather than trusting the old run: reverting the guard to clear unconditionally fails exactly the follow-up case and leaves the three original ones green — which is the shape you'd want, since it shows the change is additive to #11911's contract rather than altering it.

One pre-existing note: eslint open-sse/services/combo.ts reports 'getBootstrapLatencyMs' is defined but never used at line 277. That's on the base too (I checked with my changes stashed), not from this PR.

@diegosouzapw

Copy link
Copy Markdown
Owner

Solid, evidence-driven narrowing on top of #12013 — the measured before/after
(lkgp/auto/round-robin with the same skip) makes the bug obvious, and the new
regression test locks in the fix. Ran lkgp-stale-pin-exhaustion-11911.test.ts at your
head: 4/4 pass, including the new "a pin naming a healthy provider survives another
target being skipped" case. One thing before merge: could you add a changelog fragment
under changelog.d/fixes/? Also curious whether the remaining clearStaleLKGP
call-sites that didn't get the new target argument (e.g. inside
stopProtectedPriorityTarget) were left out deliberately or just not gotten to yet —
not a blocker, the omitted-arg path is safe by design, just want to confirm intent.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@abhisheksharma2411

Copy link
Copy Markdown
Contributor Author

Both done, and the answer to your question is "not gotten to yet" — not deliberate.

stopProtectedPriorityTarget should have had it. executeTargetGates.ts:63 has target in scope with both target.provider (bound two lines above) and target.connectionId (used at :108, :141, :185), so there was nothing stopping it. I'd updated the sites I reached through the failing paths I was reproducing and missed the ones reached through the protected-priority stop. Good catch.

All 14 call sites that have a failed target in scope now pass it — 4 in executeTargetAttempt.ts, 5 in executeTargetGates.ts (including that one), 5 in roundRobinCombo.ts. There are no 5-arg call sites left.

Changelog fragment added at changelog.d/fixes/12235-lkgp-clear-scope.md.

One thing I want to flag, because it nearly became a silent bug

Rebasing onto the staleLkgpClear.ts extraction, base's signature had gained a sixth parameter of its own — the clearLKGP test seam — while my change had also added a sixth. A textual conflict resolution would have left my call sites passing target into the seam slot, where clearPins would then call it as clear(comboName, key).

So failed is the seventh parameter and production callers pass undefined for the seam. That isn't cosmetic: tests/unit/combo/stale-lkgp-clear-13614.test.ts passes clearLKGP positionally as the 6th argument in three places, so inserting ahead of it would have broken that file. It passes 5/5 at my head.

Seven positional parameters with an undefined hole is not nice to read. If you'd rather, the natural next step is an options object for everything after tag — happy to do that here or leave it for whoever adds the eighth, your call.

Verification

lkgp-stale-pin-exhaustion-11911.test.ts + combo/stale-lkgp-clear-13614.test.ts   9 pass, 0 fail
tsc --noEmit                                                                     no combo/staleLkgp errors
eslint (4 touched files)                                                         clean

Mutation-checked on the new base rather than trusting the old run:

Mutation Result
clear the combo pin unconditionally (base behaviour) follow-up test FAILS
drop the provider match, clear on any sibling follow-up test FAILS
restored 9 pass

Both rows fail only the new case and leave base's three #11911 cases plus all five #13614 cases green — which is the shape you'd want, since it shows this narrows the behaviour without altering the contract either of those pinned.

@abhisheksharma2411

Copy link
Copy Markdown
Contributor Author

@diegosouzapw — same as on #13673: I force-pushed over your commit here earlier today and have restored it. Head is 7ba0835ac8 again, your docs(changelog): add fragment for the LKGP pin scope fix, as you left it. Cause was mine — I read author (me, because you preserved it) instead of committer (you), and a stale tracking ref meant --force-with-lease compared against the wrong baseline.

Answering your actual question, which still stands: stopProtectedPriorityTarget was not gotten to deliberately — it was an omission. executeTargetGates.ts:63 has target in scope with both target.provider (bound two lines above) and target.connectionId (used at :108, :141, :185), so nothing was stopping it. I'd updated the sites I reached through the failing paths I was reproducing and missed the ones reached through the protected-priority stop.

I did the rest of that work before realising I was overwriting you, and I've not pushed it — it would clobber your commit a second time, and which way this goes is your call, not mine. What it contains, if you want it:

  • Rebased onto current release/v3.8.51, which matters because clearStaleLKGP has since been extracted into open-sse/services/combo/staleLkgpClear.ts — this branch predates that.
  • All 14 call sites with a failed target in scope now pass it, including stopProtectedPriorityTarget. No 5-arg call sites remain.
  • One trap worth flagging regardless of what you do with the rest: the extraction gave clearStaleLKGP a sixth parameter of its own — the clearLKGP test seam — and my change had also added a sixth. A textual conflict resolution leaves callers passing target into the seam slot, where clearPins then calls it as clear(comboName, key). So failed has to be the seventh parameter; tests/unit/combo/stale-lkgp-clear-13614.test.ts passes the seam positionally in three places and would break otherwise.
  • Verified: 9/9 across lkgp-stale-pin-exhaustion-11911 + combo/stale-lkgp-clear-13614; mutation-checked (clearing unconditionally, or dropping the provider match, each fails only the new case and leaves your three fix(resilience): Stale LKGP pin survives provider/connection exhaustion — dead free-tier provider re-selected every request #11911 cases and all five fix(routing): await stale provider-pin clears and gate swallowed errors plus fire-and-forget async #13614 cases green).

Tell me which you'd prefer — I push that on top, you take it from a fresh PR, or you'd rather do it yourself — and I'll follow that. I'm not touching this branch again without you saying so.

The conflict was structural, not textual: the base extracted
clearStaleLKGP out of combo.ts into combo/staleLkgpClear.ts, gave it a
promise contract ("routing callers ignore it, tests await it") and a
positional test seam. This branch had grown a `failed` parameter in the
same slot that seam now occupies.

Took the base's structure wholesale — combo.ts stays a re-export — and
moved the pin-scoping into staleLkgpClear.ts, keeping its promise
contract and its richer warn payload. `failed` moves to slot 7 so the
seam keeps the position tests/unit/combo/stale-lkgp-clear-13614.test.ts
passes it in; all 14 call sites updated.

Also closes a coverage hole this branch always had: nothing exercised
the sibling-connection rule, so matching on provider alone survived
mutation. Two connections of the same provider are independent targets,
and one failing says nothing about the other.
@abhisheksharma2411

Copy link
Copy Markdown
Contributor Author

Merged release/v3.8.51 in and resolved — mergeable again. Thanks for the changelog fragment, that was the piece I'd left off.

Merged rather than rebased on purpose: the branch tip was your commit, and a rebase would have rewritten it.

The conflict was structural, not textual. clearStaleLKGP moved out of combo.ts into combo/staleLkgpClear.ts and picked up two things this branch predates — a promise contract ("routing callers ignore it, tests await it") and a positional test seam in slot 6, which is exactly where this branch had put failed.

So I took the base's structure wholesale rather than either side of the hunk: combo.ts stays a re-export, the pin-scoping moved into staleLkgpClear.ts keeping its promise return and its richer warn payload, and failed moved to slot 7 so stale-lkgp-clear-13614.test.ts keeps passing the seam positionally. All 14 call sites updated.

The rewrite also closed a hole this branch always had. Mutation-testing after the move, one mutant survived: replacing the connection check with true.

matching on provider alone  ->  9 pass, 0 fail   (survived)

Nothing exercised the sibling-connection rule, so "same provider, different connection" was unguarded — two connections of one provider are independent targets, and one failing says nothing about the other. Added a test for both directions (conn-B failing leaves a conn-A pin alone; conn-A failing clears it). That mutant and its over-strict opposite now both die.

315 pass / 0 fail across tests/unit/combo/, plus the LKGP suites.

@abhisheksharma2411

Copy link
Copy Markdown
Contributor Author

Answering the call-site question, and thanks for the changelog fragment — you'd already added it before I got back to this.

Every call site passes the failed target, including stopProtectedPriorityTarget. I checked the exact commit you reviewed (25c7b20) rather than just the current head, since the question was about that state:

$ git show 25c7b20:open-sse/services/combo/executeTargetGates.ts
  const stopProtectedPriorityTarget = (message: string) => {
    state.observeFailure(false, target.executionKey);
    deps.clearStaleLKGP(
      deps.combo.name, target.executionKey, deps.combo.id, deps.log, "COMBO",
      target                      // <- present

All 14 call sites, at that commit and now:

executeTargetGates.ts    5   all pass target
executeTargetAttempt.ts  4   all pass target
roundRobinCombo.ts       5   all pass target

and grep -rn "clearStaleLKGP(" open-sse src finds no caller outside services/combo/, so there's no fourth file quietly on the old signature.

So why is the parameter optional at all? Not because any current caller skips it — it's that the omitted-arg path has to keep the old unconditional behaviour for a caller that genuinely has no target in scope, so adding the argument could never be a silent behaviour change for code I hadn't looked at. With every caller now passing it, that path is effectively dead today and exists as a compatibility floor rather than an intentional exemption. Happy to make it required instead if you'd rather the type enforce it — that would be a one-line change plus the signature.

One thing that did change since your review: the branch conflicted after the merge wave, and I resolved it by merging release/v3.8.51 in rather than rebasing, because the branch tip was your commit and a rebase would have rewritten it. The conflict turned out to be structural — clearStaleLKGP moved out of combo.ts into combo/staleLkgpClear.ts and picked up a promise contract plus a positional test seam in slot 6, which is exactly where this branch had failed. I took the base's structure and moved failed to slot 7 so stale-lkgp-clear-13614.test.ts keeps passing the seam positionally; that's why the call sites now read ..., "COMBO-RR", undefined, target.

The re-port also closed a hole this branch always had: mutation-testing after the move, replacing the connection check with true survived — nothing exercised the sibling-connection rule, so "same provider, different connection" was unguarded. Added a test for both directions. 315 pass / 0 fail across tests/unit/combo/.

…Clear

clearStaleLKGP's optional `failed` reached clearPins as FailedTarget | undefined
while the parameter is FailedTarget (null-able, not undefined) — typecheck:core
on the merged tree flagged it; pass `failed ?? null`.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @abhisheksharma2411 — merging via the release merge-train. Validated in local merge-train (mt-train8e) on the devbox @ train tip b38c9340f092c8852ecfa7f6e0f9592fd6f7fc35 with the 26 sibling PRs of the owner-approved file-size rebaseline batch: typecheck:core, file-size (rebaselined entry _rebaseline_2026_09_18_merge_train_8_frozen_growth), complexity, cognitive-complexity, changelog-integrity green; changed-area node:test 722/722; vitest 479/482 where the 3 reds were 5s/20s timeouts under devbox load 18 and pass when re-run alone on the same train tip (flake class). Merged --admin per merge-gates §7.

@diegosouzapw
diegosouzapw merged commit 0f5f83c into diegosouzapw:release/v3.8.51 Sep 19, 2026
7 of 16 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…ed target (diegosouzapw#12235)

* fix(resilience): clear the combo LKGP pin only when it names the failed target

Re-applied onto current release/v3.8.51. The branch was 217 commits behind
and dispatchWithCooldownRetry / handleRoundRobinCombo have since moved out
of combo.ts, so this is a re-application, not a rebase: the definition is
still in combo.ts, and the 14 call sites now live in
combo/executeTargetAttempt.ts (4), combo/executeTargetGates.ts (5) and
combo/roundRobinCombo.ts (5). clearStaleLKGP is dependency-injected via
attemptLoopTypes.ts, so that signature takes the new parameter too.

Unchanged in substance. The target-scoped pin is still always cleared; the
combo-level pin is cleared only when it actually names the failed target's
provider, because it records whichever provider last *succeeded* and that
need not be the one failing now. Under `auto` the pin is a scoring input
rather than a hoist, so clearing unconditionally discarded a preference for
a healthy provider every time an unrelated target was skipped. Omitting
`failed` keeps the old behaviour for callers with no target in scope.

  4 regression tests pass, including "a pin naming a healthy provider
  survives another target being skipped"

  mutation: clear the combo pin unconditionally (the pre-fix behaviour)
    -> only that test fails, 3 pass

  248 tests pass across tests/unit/lkgp*, tests/unit/combo/*
  eslint clean on all five changed files (exit 0, no suppressions flag)

* docs(changelog): add fragment for the LKGP pin scope fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: abhisheksharma2411 <abhisheksharma2411@users.noreply.github.com>
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.

2 participants