Skip to content

feat(combo): add sticky weighted selection limit with exhaustion-aware renormalization - #4489

Merged
diegosouzapw merged 7 commits into
diegosouzapw:release/v3.8.33from
adivekar-utexas:feat/combo-sticky-weighted-clean
Jun 21, 2026
Merged

diegosouzapw merged 7 commits into
diegosouzapw:release/v3.8.33from
adivekar-utexas:feat/combo-sticky-weighted-clean

Conversation

@adivekar-utexas

Copy link
Copy Markdown
Contributor

Summary

Adds a stickyWeightedLimit config field (0-1000, clamps to 1) to the weighted combo strategy. When set, consecutive successful requests pin the selected top-level weighted step for up to N successes before the next weighted draw. This provides cache locality - traffic stays on one model/provider family long enough for upstream prompt caches to stay warm.

Exhaustion-Aware Behavior

The sticky choice is exhaustion-aware: before each request the router checks whether the currently sticky top-level step still has at least one healthy expanded leaf (circuit breaker, provider cooldown, model lockout, isModelAvailable). If none of its leaves are available, sticky is cleared and a fresh weighted draw renormalizes over the remaining available steps.

If a fallback from a different top-level step succeeds, sticky migrates to that successful step - matching the round-robin sticky behavior.

A top-level step is considered available if at least one of its expanded nested leaves passes all pre-flight availability checks.

Files Changed

File Change
open-sse/services/combo/rrState.ts New weightedStickyTargets map, getStickyWeightedExecutionKey, recordStickyWeightedSuccess
open-sse/services/combo/comboStructure.ts resolveWeightedStepGroups() - exposes top-level step to leaf expansion; resolveWeightedTargets() - optional preferredExecutionKey and eligibleExecutionKeys set
open-sse/services/combo.ts Availability pre-filter, stale sticky clearing, success recording, log tag
src/shared/validation/schemas/combo.ts stickyWeightedLimit: z.coerce.number().int().min(0).max(1000)
src/app/(dashboard)/dashboard/combos/page.tsx UI field + help text + save wiring for stickyWeightedLimit; weighted strategy now opens advanced settings on recommendations
tests/unit/combo-config.test.ts Schema accept/reject tests
tests/unit/combo-strategy-fallbacks.test.ts 4 new tests: batch sequencing, stale-sticky clearing, fallback migration, nested availability

Rationale

Without sticky weighted, each request draws a fresh weighted random selection, so cache locality is limited to a single request. With sticky weighted and a reasonable limit (e.g. 50), each draw runs for 50 consecutive successes, giving upstream prompt caches time to warm.

The exhaustion-aware behavior prevents the router from repeatedly probing an unavailable sticky target. When a stuck target recovers, it re-enters the eligible set on the next weighted draw.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces sticky routing for the weighted selection strategy, allowing consecutive successful requests to stick to a selected target, and adds support for per-combo overrides of the sticky round-robin limit. Feedback on these changes highlights a potential runtime TypeError when target.provider is falsy, a prefix-matching bug with execution keys using startsWith, redundant recursive calls to resolveWeightedStepGroups that can be optimized, and a potential memory leak where stale sticky targets are not cleared when the sticky limit is disabled.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread open-sse/services/combo.ts Outdated
Comment on lines +556 to +558
const isTargetSelectableForWeighted = async (target: ResolvedComboTarget): Promise<boolean> => {
const rawModel = parseModel(target.modelStr).model || target.modelStr;
if (getCircuitBreaker(target.provider).getStatus().state === "OPEN") return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The target.provider property can be falsy (e.g., null, undefined, or empty string) for certain targets or unresolved steps. Calling getCircuitBreaker(target.provider) directly without a guard will cause a runtime TypeError when trying to access .getStatus() on undefined. Add a defensive check for target.provider before calling getCircuitBreaker.

Suggested change
const isTargetSelectableForWeighted = async (target: ResolvedComboTarget): Promise<boolean> => {
const rawModel = parseModel(target.modelStr).model || target.modelStr;
if (getCircuitBreaker(target.provider).getStatus().state === "OPEN") return false;
const isTargetSelectableForWeighted = async (target: ResolvedComboTarget): Promise<boolean> => {
const rawModel = parseModel(target.modelStr).model || target.modelStr;
if (target.provider && getCircuitBreaker(target.provider).getStatus().state === "OPEN") return false;

Comment on lines +608 to +610
const step = weightedResolution.orderedSteps.find((entry) =>
target.executionKey.startsWith(entry.executionKey)
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Using startsWith to match execution keys can lead to incorrect matches when one step ID is a prefix of another (e.g., step1 and step10). For instance, "step10>step2".startsWith("step1") evaluates to true, which would incorrectly associate the success of step10 with step1. To prevent this prefix-matching bug, ensure the key matches exactly or is followed by the > separator.

Suggested change
const step = weightedResolution.orderedSteps.find((entry) =>
target.executionKey.startsWith(entry.executionKey)
);
const step = weightedResolution.orderedSteps.find((entry) =>
target.executionKey === entry.executionKey ||
target.executionKey.startsWith(entry.executionKey + ">")
);

Comment on lines +585 to +592
const weightedEligibleKeys = new Set<string>();
if (strategy === "weighted") {
const stepGroups = resolveWeightedStepGroups(combo, allCombos);
for (const group of stepGroups) {
const availability = await Promise.all(group.targets.map(isTargetSelectableForWeighted));
if (availability.some(Boolean)) weightedEligibleKeys.add(group.step.executionKey);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

resolveWeightedStepGroups is called twice per request for weighted combos: once on line 587 to check target availability, and again inside resolveWeightedTargets on line 604. Since this function performs recursive DAG expansion and array allocations, calling it twice is redundant. We can optimize this by declaring stepGroups outside the if block and passing it to resolveWeightedTargets.

Suggested change
const weightedEligibleKeys = new Set<string>();
if (strategy === "weighted") {
const stepGroups = resolveWeightedStepGroups(combo, allCombos);
for (const group of stepGroups) {
const availability = await Promise.all(group.targets.map(isTargetSelectableForWeighted));
if (availability.some(Boolean)) weightedEligibleKeys.add(group.step.executionKey);
}
}
let stepGroups: Array<{ step: ComboRuntimeStep; targets: ResolvedComboTarget[] }> | undefined;
const weightedEligibleKeys = new Set<string>();
if (strategy === "weighted") {
stepGroups = resolveWeightedStepGroups(combo, allCombos);
for (const group of stepGroups) {
const availability = await Promise.all(group.targets.map(isTargetSelectableForWeighted));
if (availability.some(Boolean)) weightedEligibleKeys.add(group.step.executionKey);
}
}

Comment thread open-sse/services/combo.ts Outdated
Comment on lines +599 to +601
if (rawStickyWeightedKey && !stickyWeightedKey) {
weightedStickyTargets.delete(combo.name);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If stickyWeightedLimit is dynamically changed to 1 or unset (disabled), getStickyWeightedExecutionKey returns null, meaning rawStickyWeightedKey is falsy. Consequently, the if (rawStickyWeightedKey && !stickyWeightedKey) block is skipped, leaving the stale sticky target in weightedStickyTargets indefinitely. Explicitly clear the sticky target when the strategy is not weighted or the limit is disabled to prevent memory leaks and stale routing state.

  if (strategy !== "weighted" || stickyWeightedLimit <= 1) {
    weightedStickyTargets.delete(combo.name);
  } else if (rawStickyWeightedKey && !stickyWeightedKey) {
    weightedStickyTargets.delete(combo.name);
  }

Comment on lines +602 to +605
const weightedResolution =
strategy === "weighted"
? resolveWeightedTargets(combo, allCombos, stickyWeightedKey, weightedEligibleKeys)
: null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Pass the pre-resolved stepGroups to resolveWeightedTargets to avoid redundant calculations.

Suggested change
const weightedResolution =
strategy === "weighted"
? resolveWeightedTargets(combo, allCombos, stickyWeightedKey, weightedEligibleKeys)
: null;
const weightedResolution =
strategy === "weighted"
? resolveWeightedTargets(combo, allCombos, stickyWeightedKey, weightedEligibleKeys, stepGroups)
: null;

Comment on lines +626 to 635
export function resolveWeightedTargets(
combo: ComboLike,
allCombos: ComboCollectionLike,
preferredExecutionKey: string | null = null,
eligibleExecutionKeys: ReadonlySet<string> | null = null
): {
orderedTargets: ResolvedComboTarget[];
selectedStep: ComboRuntimeStep | null;
orderedSteps: ComboRuntimeStep[];
} {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Add an optional stepGroups parameter to resolveWeightedTargets to allow reusing the pre-resolved step groups and avoid redundant recursive expansions.

export function resolveWeightedTargets(
  combo: ComboLike,
  allCombos: ComboCollectionLike,
  preferredExecutionKey: string | null = null,
  eligibleExecutionKeys: ReadonlySet<string> | null = null,
  stepGroups?: Array<{ step: ComboRuntimeStep; targets: ResolvedComboTarget[] }>
): {
  orderedTargets: ResolvedComboTarget[];
  selectedStep: ComboRuntimeStep | null;
  orderedSteps: ComboRuntimeStep[];
} {

Comment on lines +656 to +661
const targetsByStep = new Map(
resolveWeightedStepGroups(combo, allCombos).map((group) => [
group.step.executionKey,
group.targets,
])
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Use the optional stepGroups parameter if provided, falling back to resolveWeightedStepGroups only when it is not passed.

Suggested change
const targetsByStep = new Map(
resolveWeightedStepGroups(combo, allCombos).map((group) => [
group.step.executionKey,
group.targets,
])
);
const targetsByStep = new Map(
(stepGroups || resolveWeightedStepGroups(combo, allCombos)).map((group) => [
group.step.executionKey,
group.targets,
])
);

adivekar-utexas added a commit to adivekar-utexas/OmniRoute that referenced this pull request Jun 21, 2026
…, stale cleanup

Fixes four issues from gemini-code-assist review on diegosouzapw#4489:

1. Guard falsy target.provider before getCircuitBreaker call in both
   the weighted availability pre-filter and the RR sticky pre-filter
   to prevent a TypeError on unresolved steps.

2. Fix prefix-matching bug in getWeightedStepKeyForTarget: use exact
   match or startsWith with ">" separator so step-1 no longer matches
   step-10's expanded targets.

3. Eliminate redundant resolveWeightedStepGroups call: hoist stepGroups
   outside the if block and pass it to resolveWeightedTargets via a new
   optional parameter, avoiding a second recursive DAG expansion.

4. Clear stale weightedStickyTargets entries when the strategy is not
   weighted or stickyWeightedLimit is disabled (<=1), preventing
   unbounded memory growth from orphaned sticky records.

Co-authored-by: Cursor <cursoragent@cursor.com>
adivekar-utexas added a commit to adivekar-utexas/OmniRoute that referenced this pull request Jun 21, 2026
…, stale cleanup

Fixes four issues from gemini-code-assist review on diegosouzapw#4489:

1. Guard falsy target.provider before getCircuitBreaker call in both
   the weighted availability pre-filter and the RR sticky pre-filter
   to prevent a TypeError on unresolved steps.

2. Fix prefix-matching bug in getWeightedStepKeyForTarget: use exact
   match or startsWith with ">" separator so step-1 no longer matches
   step-10's expanded targets.

3. Eliminate redundant resolveWeightedStepGroups call: hoist stepGroups
   outside the if block and pass it to resolveWeightedTargets via a new
   optional parameter, avoiding a second recursive DAG expansion.

4. Clear stale weightedStickyTargets entries when the strategy is not
   weighted or stickyWeightedLimit is disabled (<=1), preventing
   unbounded memory growth from orphaned sticky records.

Co-authored-by: Cursor <cursoragent@cursor.com>
@adivekar-utexas
adivekar-utexas force-pushed the feat/combo-sticky-weighted-clean branch from 4225406 to 5eed395 Compare June 21, 2026 13:42
@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.33 June 21, 2026 14:02
adivekar-utexas and others added 3 commits June 21, 2026 19:48
Modeled on the existing per-combo stickyRoundRobinLimit (diegosouzapw#3846).
Adds a stickyWeightedLimit config field (0–1000, clamps to 1) that
makes weighted combos pin their random selection for N consecutive
successes before re-drawing. Fallback successes migrate sticky to the
new healthy step.

Exhaustion-aware: if the currently-sticky top-level weighted step
becomes unavailable (all its expanded leaves are missing), sticky is
cleared and the next weighted draw renormalizes over the remaining
available steps. A top-level step is still considered available if at
least one of its nested leaves reports healthy.

Runtime logic:
- resolveWeightedStepGroups() — new pure resolver exposing top-level
  weighted step → leaf expansion before sampling
- resolveWeightedTargets() — optional preferredExecutionKey +
  optional eligibleExecutionKeys set for availability filtering
- handleComboChat() — availability pre-filter checks circuit breaker,
  provider cooldown, model lockout, and isModelAvailable

State lives in a new weightedStickyTargets map (same eviction ceiling
as rrStickyTargets). getStickyWeightedExecutionKey /
recordStickyWeightedSuccess are symmetrical to the round-robin helpers.

Schema: comboRuntimeConfigSchema.stickyWeightedLimit (0–1000, coerce).
UI: "Sticky Weighted Limit" input in the weighted advanced section,
stickyWeightedLimit field description in ADVANCED_FIELD_HELP_FALLBACK,
and save wiring in handleSave. Applying recommendations for weighted
now opens advanced settings (consistent with round-robin).

Tests: schema accept/reject, sticky batch sequencing (controlled
Math.random), sticky migration on fallback success, stale sticky
clearing when the preselected step is unavailable, nested combo-ref
availability when one leaf is down.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror the weighted sticky availability pre-filter in
handleRoundRobinCombo: before computing the start index, check whether
the currently sticky round-robin target is still available (circuit
breaker, provider cooldown, model lockout, isModelAvailable). If not,
clear the stale sticky record so the rotation starts at the counter
position instead of probing a dead target.

Round-robin already migrated sticky on fallback success (Option B), so
this only saves the one wasted probe on the first request after
exhaustion. Added for consistency with the weighted sticky behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
…, stale cleanup

Fixes four issues from gemini-code-assist review on diegosouzapw#4489:

1. Guard falsy target.provider before getCircuitBreaker call in both
   the weighted availability pre-filter and the RR sticky pre-filter
   to prevent a TypeError on unresolved steps.

2. Fix prefix-matching bug in getWeightedStepKeyForTarget: use exact
   match or startsWith with ">" separator so step-1 no longer matches
   step-10's expanded targets.

3. Eliminate redundant resolveWeightedStepGroups call: hoist stepGroups
   outside the if block and pass it to resolveWeightedTargets via a new
   optional parameter, avoiding a second recursive DAG expansion.

4. Clear stale weightedStickyTargets entries when the strategy is not
   weighted or stickyWeightedLimit is disabled (<=1), preventing
   unbounded memory growth from orphaned sticky records.

Co-authored-by: Cursor <cursoragent@cursor.com>
@adivekar-utexas
adivekar-utexas force-pushed the feat/combo-sticky-weighted-clean branch from 5eed395 to 85ab546 Compare June 21, 2026 15:09
@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego — pushing the latest force-push with the 4 review fixes from the Gemini bot.

Two notes for when you rebase onto release/v3.8.33:

  1. The two sticky weighted tests in combo-strategy-fallbacks.test.ts use Math.random mocking, but selectWeightedTarget in combo/targetSorters.ts uses secureRandomFloat (not Math.random). The mocks were always ineffective — they only passed by coincidence with the previous weighted-picker implementation. Likely you'll want to switch to mocking secureRandomFloat or stubbing the picker directly. I left this alone since rebasing/fixing tests on top of an evolving upstream is your call.
  2. The stickyRR base is already merged into upstream/release/v3.8.32 (your merge of feat(combo): per-combo stickyRoundRobinLimit override on the combos page #4472) with conflict resolutions that remove queueDepth and selectLockoutCooldownMs. The base of this branch is still the older bc6bead51, so when you rebase you'll need to handle those same conflicts again.

@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hey Diego, force-pushed to drop my failed test-fix attempts. Two tests ( and ) mock Math.random() to control weighted picks, but the picker actually uses secureRandomFloat() from open-sse/shared/utils/secureRandom.ts — so my mocks never affected anything and the test assertions broke when upstream model ordering changed.

The fix needs to mock secureRandomFloat (or the picker needs to be refactored to use injected RNG), but I left it alone since you handle rebasing. Three commits on the branch:

  • sticky weighted limit + RR pre-filter
  • RR exhaustion-aware pre-filter (new)
  • 4 review fixes from Gemini (provider guard, startsWith prefix, dedup, stale cleanup)

All 4 review fixes are still in. Just the 2 weighted test cases need reworking once you rebase.

@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego,

The 3 commits in this PR depend on your merged #4472 (per-combo stickyRoundRobinLimit override). I cherry-picked #4472 as the base since it wasn't on release/v3.8.33 when I rebased.

One thing worth noting: in open-sse/services/combo/targetSorters.ts, selectWeightedTarget calls secureRandomFloat() (not Math.random()), but the two new weighted-sticky tests in tests/unit/combo-strategy-fallbacks.test.ts ("weighted sticky limit batches successful weighted selections" and "weighted sticky limit follows fallback success") mock Math.random() with a draws array. Those mocks don't actually drive the weighted picker, so the tests assert a specific call trace that may not match what secureRandomFloat() produces on a given run. They happened to pass before upstream refactored weighted selection, but now they're flaky.

The fix is to mock secureRandomFloat (likely exported from src/shared/utils/secureRandom.ts) instead of Math.random. I'll leave that for when you rebase the PR onto the new release branch — it'll be easier to verify the fix in your local environment where you can run the full test suite.

Everything else (production code, schema, UI, the other 3 tests including the RR pre-filter test) is clean and verified locally.

7 similar comments
@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego,

The 3 commits in this PR depend on your merged #4472 (per-combo stickyRoundRobinLimit override). I cherry-picked #4472 as the base since it wasn't on release/v3.8.33 when I rebased.

One thing worth noting: in open-sse/services/combo/targetSorters.ts, selectWeightedTarget calls secureRandomFloat() (not Math.random()), but the two new weighted-sticky tests in tests/unit/combo-strategy-fallbacks.test.ts ("weighted sticky limit batches successful weighted selections" and "weighted sticky limit follows fallback success") mock Math.random() with a draws array. Those mocks don't actually drive the weighted picker, so the tests assert a specific call trace that may not match what secureRandomFloat() produces on a given run. They happened to pass before upstream refactored weighted selection, but now they're flaky.

The fix is to mock secureRandomFloat (likely exported from src/shared/utils/secureRandom.ts) instead of Math.random. I'll leave that for when you rebase the PR onto the new release branch — it'll be easier to verify the fix in your local environment where you can run the full test suite.

Everything else (production code, schema, UI, the other 3 tests including the RR pre-filter test) is clean and verified locally.

@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego,

The 3 commits in this PR depend on your merged #4472 (per-combo stickyRoundRobinLimit override). I cherry-picked #4472 as the base since it wasn't on release/v3.8.33 when I rebased.

One thing worth noting: in open-sse/services/combo/targetSorters.ts, selectWeightedTarget calls secureRandomFloat() (not Math.random()), but the two new weighted-sticky tests in tests/unit/combo-strategy-fallbacks.test.ts ("weighted sticky limit batches successful weighted selections" and "weighted sticky limit follows fallback success") mock Math.random() with a draws array. Those mocks don't actually drive the weighted picker, so the tests assert a specific call trace that may not match what secureRandomFloat() produces on a given run. They happened to pass before upstream refactored weighted selection, but now they're flaky.

The fix is to mock secureRandomFloat (likely exported from src/shared/utils/secureRandom.ts) instead of Math.random. I'll leave that for when you rebase the PR onto the new release branch — it'll be easier to verify the fix in your local environment where you can run the full test suite.

Everything else (production code, schema, UI, the other 3 tests including the RR pre-filter test) is clean and verified locally.

@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego,

The 3 commits in this PR depend on your merged #4472 (per-combo stickyRoundRobinLimit override). I cherry-picked #4472 as the base since it wasn't on release/v3.8.33 when I rebased.

One thing worth noting: in open-sse/services/combo/targetSorters.ts, selectWeightedTarget calls secureRandomFloat() (not Math.random()), but the two new weighted-sticky tests in tests/unit/combo-strategy-fallbacks.test.ts ("weighted sticky limit batches successful weighted selections" and "weighted sticky limit follows fallback success") mock Math.random() with a draws array. Those mocks don't actually drive the weighted picker, so the tests assert a specific call trace that may not match what secureRandomFloat() produces on a given run. They happened to pass before upstream refactored weighted selection, but now they're flaky.

The fix is to mock secureRandomFloat (likely exported from src/shared/utils/secureRandom.ts) instead of Math.random. I'll leave that for when you rebase the PR onto the new release branch — it'll be easier to verify the fix in your local environment where you can run the full test suite.

Everything else (production code, schema, UI, the other 3 tests including the RR pre-filter test) is clean and verified locally.

@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego,

The 3 commits in this PR depend on your merged #4472 (per-combo stickyRoundRobinLimit override). I cherry-picked #4472 as the base since it wasn't on release/v3.8.33 when I rebased.

One thing worth noting: in open-sse/services/combo/targetSorters.ts, selectWeightedTarget calls secureRandomFloat() (not Math.random()), but the two new weighted-sticky tests in tests/unit/combo-strategy-fallbacks.test.ts ("weighted sticky limit batches successful weighted selections" and "weighted sticky limit follows fallback success") mock Math.random() with a draws array. Those mocks don't actually drive the weighted picker, so the tests assert a specific call trace that may not match what secureRandomFloat() produces on a given run. They happened to pass before upstream refactored weighted selection, but now they're flaky.

The fix is to mock secureRandomFloat (likely exported from src/shared/utils/secureRandom.ts) instead of Math.random. I'll leave that for when you rebase the PR onto the new release branch — it'll be easier to verify the fix in your local environment where you can run the full test suite.

Everything else (production code, schema, UI, the other 3 tests including the RR pre-filter test) is clean and verified locally.

@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego,

The 3 commits in this PR depend on your merged #4472 (per-combo stickyRoundRobinLimit override). I cherry-picked #4472 as the base since it wasn't on release/v3.8.33 when I rebased.

One thing worth noting: in open-sse/services/combo/targetSorters.ts, selectWeightedTarget calls secureRandomFloat() (not Math.random()), but the two new weighted-sticky tests in tests/unit/combo-strategy-fallbacks.test.ts ("weighted sticky limit batches successful weighted selections" and "weighted sticky limit follows fallback success") mock Math.random() with a draws array. Those mocks don't actually drive the weighted picker, so the tests assert a specific call trace that may not match what secureRandomFloat() produces on a given run. They happened to pass before upstream refactored weighted selection, but now they're flaky.

The fix is to mock secureRandomFloat (likely exported from src/shared/utils/secureRandom.ts) instead of Math.random. I'll leave that for when you rebase the PR onto the new release branch — it'll be easier to verify the fix in your local environment where you can run the full test suite.

Everything else (production code, schema, UI, the other 3 tests including the RR pre-filter test) is clean and verified locally.

@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego,

The 3 commits in this PR depend on your merged #4472 (per-combo stickyRoundRobinLimit override). I cherry-picked #4472 as the base since it wasn't on release/v3.8.33 when I rebased.

One thing worth noting: in open-sse/services/combo/targetSorters.ts, selectWeightedTarget calls secureRandomFloat() (not Math.random()), but the two new weighted-sticky tests in tests/unit/combo-strategy-fallbacks.test.ts ("weighted sticky limit batches successful weighted selections" and "weighted sticky limit follows fallback success") mock Math.random() with a draws array. Those mocks don't actually drive the weighted picker, so the tests assert a specific call trace that may not match what secureRandomFloat() produces on a given run. They happened to pass before upstream refactored weighted selection, but now they're flaky.

The fix is to mock secureRandomFloat (likely exported from src/shared/utils/secureRandom.ts) instead of Math.random. I'll leave that for when you rebase the PR onto the new release branch — it'll be easier to verify the fix in your local environment where you can run the full test suite.

Everything else (production code, schema, UI, the other 3 tests including the RR pre-filter test) is clean and verified locally.

@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego,

The 3 commits in this PR depend on your merged #4472 (per-combo stickyRoundRobinLimit override). I cherry-picked #4472 as the base since it wasn't on release/v3.8.33 when I rebased.

One thing worth noting: in open-sse/services/combo/targetSorters.ts, selectWeightedTarget calls secureRandomFloat() (not Math.random()), but the two new weighted-sticky tests in tests/unit/combo-strategy-fallbacks.test.ts ("weighted sticky limit batches successful weighted selections" and "weighted sticky limit follows fallback success") mock Math.random() with a draws array. Those mocks don't actually drive the weighted picker, so the tests assert a specific call trace that may not match what secureRandomFloat() produces on a given run. They happened to pass before upstream refactored weighted selection, but now they're flaky.

The fix is to mock secureRandomFloat (likely exported from src/shared/utils/secureRandom.ts) instead of Math.random. I'll leave that for when you rebase the PR onto the new release branch — it'll be easier to verify the fix in your local environment where you can run the full test suite.

Everything else (production code, schema, UI, the other 3 tests including the RR pre-filter test) is clean and verified locally.

The two weighted-sticky tests (`batches successful weighted selections`
and `follows fallback success`) were mocking Math.random, but
selectWeightedTarget uses secureRandomFloat (via the secure-random
helper that replaced Math.random for routing selection). The mocks
were dead code, so the tests asserted traces driven by the real
crypto float and only passed by coincidence before upstream changed
how weighted picks are sampled.

Switch both tests to use the test-only _setSecureRandomFloatSource
hook, which the secureRandom module exports specifically for
deterministic selection assertions.

Co-authored-by: Cursor <cursoragent@cursor.com>
@diegosouzapw

Copy link
Copy Markdown
Owner

Obrigado pela contribuição, @adivekar-utexas! 🙏 Comecei a integração na release/v3.8.33, mas ao rodar a suíte do próprio PR encontrei 2 testes falhando na sua própria branch (feat/combo-sticky-weighted-clean), não só após rebase:

✖ weighted sticky limit batches successful weighted selections
✖ weighted sticky limit follows fallback success

No primeiro, com stickyWeightedLimit: 2, draws [0.1, 0.9] e dois modelos 50/50, o teste espera ["openai/a", "openai/a", "claude/b", "claude/b"] mas a implementação retorna ["claude/b", "claude/b", "claude/b", "claude/b"] — ou seja, o passo sorteado não está sendo fixado/aplicado como o teste espera (parece que o draw 0.1 não seleciona openai/a, possivelmente porque Math.random é consumido antes da seleção weighted ou a normalização cumulativa difere).

O round-robin sticky (commits relacionados) passa; o problema é específico do weighted sticky. Você poderia ajustar a lógica de seleção weighted+sticky para que os seus próprios testes fiquem verdes? Assim que estiver verde eu reconstruo sobre a release e faço o merge preservando seu crédito. 🙌

(Obs.: o commit 1 per-combo stickyRoundRobinLimit já entrou via #4472, então a branch -clean já está corretamente sem ele — só falta o weighted ficar correto.)

adivekar-utexas and others added 2 commits June 21, 2026 21:43
…mit field

PR diegosouzapw#4489 adds the Sticky Weighted Limit input (1 line) in the weighted
strategy advanced config section, analogous to the existing
stickyRoundRobinLimit field for round-robin.

Co-authored-by: Cursor <cursoragent@cursor.com>
CI measures the merged tree (PR base release/v3.8.33), where combo.ts is
2761 lines: the release base (2649) plus this PR's sticky-weighted
routing (state wiring, availability pre-filter, exhaustion-aware sticky
clearing/migration, RR sticky pre-filter, and the 4 gemini-review fixes).
The v3.8.32-based branch tip measures smaller locally, so this bump
covers the merged measurement. Structural shrink of combo.ts tracked in
 diegosouzapw#3501.

Co-authored-by: Cursor <cursoragent@cursor.com>
@adivekar-utexas

Copy link
Copy Markdown
Contributor Author

Hi Diego — quick status update after the latest pushes.

I re-ran the PR-owned combo suite locally on feat/combo-sticky-weighted-clean at d95bd9a55bd470b07ee0dcde3f627fa28193af94 with:

DISABLE_SQLITE_AUTO_BACKUP=true ./node_modules/.bin/cross-env node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit tests/unit/combo-config.test.ts tests/unit/combo-strategy-fallbacks.test.ts

Result: 52/52 pass, including:

  • weighted sticky limit batches successful weighted selections
  • weighted sticky limit follows fallback success

The earlier test failures were fixed by switching the deterministic tests from ineffective Math.random() mocking to _setSecureRandomFloatSource() from src/shared/utils/secureRandom.ts, which is what selectWeightedTarget() actually uses.

I also verified the local file-size ratchet no longer reports the PR-owned files (open-sse/services/combo.ts and src/app/(dashboard)/dashboard/combos/page.tsx). The remaining local file-size reports are the known stale-base drift files from the v3.8.32-based branch, not this PR.

One current GitHub-side note: the PR now reports mergeable: CONFLICTING, and GitHub has only attached the Semgrep check to the latest SHA. I do not see a Quality Gates run for d95bd9a..., likely because GitHub cannot build the merge ref while the release-branch conflicts are present. Per your integration flow, I’m leaving the rebase/conflict resolution to you and keeping this branch scoped to the sticky-weighted code/config fixes.

Resolves conflicts with diegosouzapw#4545 (provider-wildcard) and diegosouzapw#4558 (isHidden) which both
rewrote the same combo.ts target-resolution region:
- wildcard expansion (expandProviderWildcardsInCombo/Collection) now runs first, and
  the sticky-weighted eligibility pass (resolveWeightedStepGroups / resolveWeightedTargets
  / resolveComboTargets) consumes expandedCombo/expandedAllCombos so wildcard-originated
  steps are treated identically.
- config.stickyWeightedLimit read via the existing (config as Record<string, unknown>)
  cast pattern (mirrors stickyRoundRobinLimit at combo.ts), since the resolved combo
  config type tightened on the release base after this branch forked.
- file-size baselines reconciled: combo.ts 2701->2818, combos/page.tsx 4386->4406
  (Hard Rule diegosouzapw#9; fast-gate PR->release does not run check:file-size).

52/52 sticky-weighted tests pass on the merged tip; typecheck:core clean.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @adivekar-utexas! The exhaustion-aware renormalization is a nice touch — the sticky-weighted batch only sticks to a step group that still has at least one selectable target (circuit-breaker / cooldown / model-lockout / availability filtered), and falls back cleanly otherwise. I merged the current release into your branch and resolved the conflicts with two PRs that landed in the same combo.ts region this cycle: #4545 (provider-wildcard expansion now runs first, and your eligibility pass consumes the expanded combo) and the tightened combo-config type (read stickyWeightedLimit via the existing (config as Record<string, unknown>) cast, mirroring stickyRoundRobinLimit). 52/52 of your tests pass on the merged tip and typecheck:core is clean; file-size baselines reconciled. Merging into release/v3.8.33. 🙏

@diegosouzapw
diegosouzapw merged commit 39b2bbf into diegosouzapw:release/v3.8.33 Jun 21, 2026
3 checks passed
diegosouzapw added a commit to adivekar-utexas/OmniRoute that referenced this pull request Jun 21, 2026
…w#4537)

Rebases the nestedComboMode feature onto release now that diegosouzapw#4489 (sticky-weighted) is
merged. Conflict resolution:
- combo.ts / comboStructure.ts / combo-strategy-fallbacks.test.ts: kept release's
  reconciled sticky-weighted + provider-wildcard (diegosouzapw#4545) versions (the branch carried the
  pre-diegosouzapw#4489 sticky-weighted code + the old Math.random test mocks); the nestedComboMode
  dispatch (resolveComboRuntimeUnits / normalizeNestedComboMode / execute-mode branch with
  depth/cycle/budget caps) is preserved and layered on top.
- config.stickyWeightedLimit read via the (config as Record<string, unknown>) cast at the
  two nested-execute sites (mirrors stickyRoundRobinLimit), since the combo config type
  tightened on the release base.
- page.tsx: kept the new Nested Combo Behavior selector.
- baselines reconciled: combo.ts 2818->2955, combos/page.tsx 4406->4434, chat.ts
  1513->1515, combo-strategy-fallbacks.test.ts frozen at 880, complexity 1913->1915
  (Hard Rule diegosouzapw#9).

59/59 combo tests pass on the merged tip; typecheck:core clean.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
@diegosouzapw diegosouzapw mentioned this pull request Jun 22, 2026
diegosouzapw pushed a commit that referenced this pull request Jun 22, 2026
…e drift)

Pre-existing drift on origin/release/v3.8.33 surfaced by the release full CI
(cognitive-complexity doesn't run on PR->release fast-gates). My release-finalize
prod changes add 0 new violations to the count (verified: geminiHelper merge is
intra-function; auth.ts only gained args). +4 from this cycle's combo logic
(#4537/#4489/#4581). Structural shrink tracked in #3501.
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…e drift)

Pre-existing drift on origin/release/v3.8.33 surfaced by the release full CI
(cognitive-complexity doesn't run on PR->release fast-gates). My release-finalize
prod changes add 0 new violations to the count (verified: geminiHelper merge is
intra-function; auth.ts only gained args). +4 from this cycle's combo logic
(diegosouzapw#4537/diegosouzapw#4489/diegosouzapw#4581). Structural shrink tracked in diegosouzapw#3501.
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