refactor(combo): extract round-robin sticky state to combo/rrState.ts (QG v2 Fase 9 T5 D7a) (#4196)

Integrated into release/v3.8.29 — D7a round-robin sticky state extracted byte-identically to combo/rrState.ts; reconciled with #4194 (baseline combo.ts -> 3398, resolving the over-cap left by #4194).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-18 17:03:47 -03:00
committed by GitHub
parent 7e534d6a86
commit f06a90d37f
3 changed files with 81 additions and 52 deletions

View File

@@ -11,6 +11,7 @@
"_rebaseline_2026_06_18_qg9_combo_split_d5": "QG v2 Fase 9 T5 D5: combo.ts 4589->4430 — target sorters extracted byte-identically to the new open-sse/services/combo/targetSorters.ts (<cap, normalizeModelEntry/selectWeightedTarget/orderTargetsForWeightedFallback/sortModelsByCost/sortTargetsByCost/sortModelsByUsage/sortTargetsByUsage/getP2CTargetScore/orderTargetsByPowerOfTwoChoices). None were ever public, so combo.ts imports back only the six still called by code that stays (no re-export). getComboStepTarget/getComboStepWeight imports dropped from combo.ts (only normalizeModelEntry used them). Pure move, no logic change.",
"_rebaseline_2026_06_18_qg9_combo_split_d6": "QG v2 Fase 9 T5 D6: combo.ts 4430->3819 — combo structure resolution extracted byte-identically to the new open-sse/services/combo/comboStructure.ts (638, <cap): runtime-step normalization, nested-combo/DAG expansion (resolveNestedComboTargets/expandRuntimeStep/getComboFromData/getComboModelsFromData/validateComboDAG/resolveNestedComboModels), weighted/direct resolution (resolveComboTargets/resolveWeightedTargets/getDirectComboTargets + composite-tier ordering), request-compatibility filtering (filterTargetsByRequestCompatibility + deriveRequestCompatibilityRequirements/getTargetCompatibilityFailures and their private helpers) and context-size sorting. Shared dedupeTargetsByExecutionKey + the toTrimmedString/toComboLike/getCombosArray normalizers also moved (dedupe to comboData.ts since both combo.ts and comboStructure use it). The 7 previously-public symbols are re-exported from combo.ts; the ~20 external consumers (chatCore.ts, /api/combos routes, embeddings, usage, catalog) are unchanged. shadowRouting.ts's resolveNestedComboTargets import redirected from the barrel to ./comboStructure.ts (removing the D4 temporary edge). Orphaned combo.ts imports dropped (getComboModelString/normalizeComboStep, getModelContextLimit, getResolvedModelCapabilities, getTargetProvider, MAX_COMBO_DEPTH, and 6 now-unused type imports). Pure move, no logic change.",
"_rebaseline_2026_06_18_qg9_combo_split_d8": "QG v2 Fase 9 T5 D8 (reduced): combo.ts 3819->3432 — auto-strategy scoring/intent/tag-routing extracted byte-identically to the new open-sse/services/combo/autoStrategy.ts (434, <cap): the quota-soft execution-candidate registry (the single _activeExecutionCandidates Map + setCandidateQuotaSoftPenalty/_registerExecutionCandidates/_unregisterExecutionCandidates, kept together for state cohesion), QUOTA_SOFT_DEPRIORITIZE_FACTOR, scoreAutoTargets, expandAutoComboCandidatePool, intent extraction (toTextContent/extractPromptForIntent/mapIntentToTaskType/toStringArray/getIntentConfig), applyRequestTagRouting and deriveComboSessionKey. buildAutoCandidates + its two private-only helpers calculateTargetContextAffinity/getBootstrapLatencyMs (and the DEFAULT_MODEL_P95_MS/MIN_HISTORY_SAMPLES/OUTPUT_TOKEN_RATIO consts they own) were DELIBERATELY KEPT in combo.ts: buildAutoCandidates is the sole user of the internal reset-window helpers (resolveResetWindowConfig/fetchResetAwareQuotaWithCache/calculateResetWindowAffinity + the ResetWindowConfig type), so keeping it there leaves those helpers private (no export) and avoids a combo <-> autoStrategy import cycle (a later task can move the reset-window block out of combo.ts without breaking autoStrategy). autoStrategy.ts never imports from the combo barrel. dedupeTargetsByExecutionKey was already in comboData.ts (D6) and is NOT re-moved. QUOTA_SOFT_DEPRIORITIZE_FACTOR + setCandidateQuotaSoftPenalty stay re-exported from combo.ts for chatCore.ts's dynamic import(\"../services/combo\"); scoreAutoTargets/expandAutoComboCandidatePool keep their public re-export too. Orphaned combo.ts imports dropped where moved-out symbols stopped being referenced; ProviderCandidate/AutoProviderCandidate/HistoricalLatencyStatsEntry types are imported into combo.ts for buildAutoCandidates. Pure move, no logic change.",
"_rebaseline_2026_06_18_qg9_combo_split_d7a": "QG v2 Fase 9 T5 D7a: combo.ts 3440->3398 (wc -l 3397 + 1, stacked on #4194 which added the passthrough-quota guards) — round-robin sticky state extracted byte-identically to the new open-sse/services/combo/rrState.ts (71, <cap). Moved exactly the 6 RR-state symbols: MAX_RR_COUNTERS, the two mutable module Maps rrCounters (new Map<string,number>) + rrStickyTargets, and the three helpers clampStickyRoundRobinTargetLimit/getStickyRoundRobinStartIndex/recordStickyRoundRobinSuccess. STATE COHESION: rrCounters and rrStickyTargets remain SINGLE instances defined once in rrState.ts; combo.ts imports the same references back and keeps mutating them directly in orderTargetsByResetAwareQuota/orderTargetsByResetWindow/handleRoundRobinCombo (no Map duplicated — sticky-3 round-robin behavior preserved, guarded by combo-routing-e2e.test.ts). The quota state left behind (MAX_RESET_AWARE_CACHE/resetAwareConnectionCache/resetAwareQuotaCache) stays in combo.ts for a later task (D7b). None of the 6 were ever public, so combo.ts imports all six back (no re-export). rrState.ts never imports from the combo barrel; it imports only ResolvedComboTarget from ./types.ts. Pure move, no logic change.",
"_rebaseline_2026_06_18_8_2_sliding_window": "Fase 8.2 own growth: rateLimitManager.ts 1017->1022 (+5 = one import + one `await awaitProviderDefaultSlot(...)` call + a 2-line comment at the existing withRateLimit chokepoint). All sliding-window logic was extracted to the new open-sse/services/providerDefaultRateLimit.ts + open-sse/services/slidingWindowLimiter.ts (both <cap), NOT inlined. Thin wiring only; not further shrinkable.",
"_rebaseline_2026_06_18_8_1_no_thinking_alias": "Fase 8.1 own growth: catalog.ts 1435->1440 (+5 = appendNoThinkingVariants(finalModels) call + comment at the existing finalModels chokepoint) and chat.ts 1458->1471 (+13 = applyNoThinkingAlias(body) call + comment right after body.model is read, before model resolution). All real logic lives in the new open-sse/utils/noThinkingAlias.ts (<cap); both edits are thin wiring of tested helpers at the single correct integration point in each file. Not extractable.",
"_rebaseline_2026_06_18_4_4_midstream": "Fase 4.4 own growth: chatCore.ts 5980->6009 (+29 at the existing streaming-return chokepoint = capture streamRecovery.continueMidStream alongside .enabled; refactor the early-retry reopen thunk into a shared runUpstreamStream(body) helper — net DRY — and add the gated continueStream(assistantSoFar) thunk that re-runs the upstream with makeContinuationBody(bodyToSend, …), plus the onContinue log). All continuation logic (scanOpenAiSseText, makeContinuationBody, trimContinuationOverlap, the createRecoverableStream continuation path) lives in open-sse/services/streamRecovery.ts (<cap). Closes over the same per-attempt executor locals as the existing reopen; not extractable without hiding the dispatch boundary. Opt-in (default OFF).",
@@ -76,7 +77,7 @@
"open-sse/services/batchProcessor.ts": 828,
"open-sse/services/browserBackedChat.ts": 850,
"open-sse/services/claudeCodeCompatible.ts": 1202,
"open-sse/services/combo.ts": 3432,
"open-sse/services/combo.ts": 3398,
"open-sse/services/rateLimitManager.ts": 1035,
"open-sse/services/tokenRefresh.ts": 1997,
"open-sse/services/usage.ts": 3408,

View File

@@ -99,6 +99,14 @@ import type {
HistoricalLatencyStatsEntry,
} from "./combo/types.ts";
import {
MAX_RR_COUNTERS,
rrCounters,
rrStickyTargets,
clampStickyRoundRobinTargetLimit,
getStickyRoundRobinStartIndex,
recordStickyRoundRobinSuccess,
} from "./combo/rrState.ts";
import { validateResponseQuality, toRetryAfterDisplayValue } from "./combo/validateQuality.ts";
import {
TRANSIENT_FOR_SEMAPHORE,
@@ -186,15 +194,8 @@ type QuotaFetchCacheConfig = {
};
type ResetWindowConfig = ReturnType<typeof resolveResetWindowConfig>;
// In-memory atomic counter per combo for round-robin distribution
// Resets on server restart (by design — no stale state)
// Eviction limits to prevent unbounded memory growth
const MAX_RR_COUNTERS = 500;
const MAX_RESET_AWARE_CACHE = 200;
const rrCounters = new Map<string, number>();
const rrStickyTargets = new Map<string, { executionKey: string; successCount: number }>();
const resetAwareConnectionCache = new Map<
string,
{ fetchedAt: number; connections: Array<Record<string, unknown>> }
@@ -204,50 +205,6 @@ const resetAwareQuotaCache = new Map<
{ fetchedAt: number; quota: unknown; refreshPromise: Promise<unknown> | null }
>();
function clampStickyRoundRobinTargetLimit(value: unknown): number {
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) return 1;
return Math.min(Math.max(Math.floor(numericValue), 1), 1000);
}
function getStickyRoundRobinStartIndex(
comboName: string,
targets: ResolvedComboTarget[],
stickyLimit: number
): { startIndex: number; counter: number } {
const sticky = rrStickyTargets.get(comboName);
const stickyIndex = sticky
? targets.findIndex((target) => target.executionKey === sticky.executionKey)
: -1;
if (stickyLimit > 1 && sticky && stickyIndex >= 0 && sticky.successCount < stickyLimit) {
return { startIndex: stickyIndex, counter: rrCounters.get(comboName) || 0 };
}
const counter = rrCounters.get(comboName) || 0;
return { startIndex: counter % targets.length, counter };
}
function recordStickyRoundRobinSuccess(
comboName: string,
target: ResolvedComboTarget,
stickyLimit: number,
targets: ResolvedComboTarget[]
): void {
const sticky = rrStickyTargets.get(comboName);
const successCount = sticky?.executionKey === target.executionKey ? sticky.successCount + 1 : 1;
if (successCount >= stickyLimit) {
const servedIndex = targets.findIndex((entry) => entry.executionKey === target.executionKey);
rrCounters.set(
comboName,
servedIndex >= 0 ? servedIndex + 1 : (rrCounters.get(comboName) || 0) + 1
);
rrStickyTargets.delete(comboName);
return;
}
rrStickyTargets.set(comboName, { executionKey: target.executionKey, successCount });
}
function finiteNumberOrNull(value: unknown): number | null {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : null;

View File

@@ -0,0 +1,71 @@
/**
* Round-robin sticky state for combo routing.
*
* Holds the two mutable module-level Maps that back round-robin distribution
* (`rrCounters`) and sticky round-robin target affinity (`rrStickyTargets`),
* plus the helpers that read/write them. Extracted byte-identically from
* combo.ts (QG v2 Fase 9 T5 D7a).
*
* State cohesion: these two Maps MUST remain single instances. combo.ts imports
* the same references back and mutates them directly (orderTargetsByResetAwareQuota,
* orderTargetsByResetWindow, handleRoundRobinCombo) — never duplicate a Map.
*
* Pure leaf: this module never imports from the combo barrel.
*/
import type { ResolvedComboTarget } from "./types.ts";
// In-memory atomic counter per combo for round-robin distribution
// Resets on server restart (by design — no stale state)
// Eviction limits to prevent unbounded memory growth
export const MAX_RR_COUNTERS = 500;
export const rrCounters = new Map<string, number>();
export const rrStickyTargets = new Map<
string,
{ executionKey: string; successCount: number }
>();
export function clampStickyRoundRobinTargetLimit(value: unknown): number {
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) return 1;
return Math.min(Math.max(Math.floor(numericValue), 1), 1000);
}
export function getStickyRoundRobinStartIndex(
comboName: string,
targets: ResolvedComboTarget[],
stickyLimit: number
): { startIndex: number; counter: number } {
const sticky = rrStickyTargets.get(comboName);
const stickyIndex = sticky
? targets.findIndex((target) => target.executionKey === sticky.executionKey)
: -1;
if (stickyLimit > 1 && sticky && stickyIndex >= 0 && sticky.successCount < stickyLimit) {
return { startIndex: stickyIndex, counter: rrCounters.get(comboName) || 0 };
}
const counter = rrCounters.get(comboName) || 0;
return { startIndex: counter % targets.length, counter };
}
export function recordStickyRoundRobinSuccess(
comboName: string,
target: ResolvedComboTarget,
stickyLimit: number,
targets: ResolvedComboTarget[]
): void {
const sticky = rrStickyTargets.get(comboName);
const successCount = sticky?.executionKey === target.executionKey ? sticky.successCount + 1 : 1;
if (successCount >= stickyLimit) {
const servedIndex = targets.findIndex((entry) => entry.executionKey === target.executionKey);
rrCounters.set(
comboName,
servedIndex >= 0 ? servedIndex + 1 : (rrCounters.get(comboName) || 0) + 1
);
rrStickyTargets.delete(comboName);
return;
}
rrStickyTargets.set(comboName, { executionKey: target.executionKey, successCount });
}