feat(combo): quota-weighted routing — skip empty accounts, draw by leftover - #12789
diegosouzapw merged 27 commits into
Conversation
|
Local review of the product commits (the four I stopped the run after two L1 cycles because the finding fingerprints did not overlap. That is oscillation, not a clean 3-cycle pass. I went through the ten candidates against the spec and the source instead of letting it burn another round. None of them is a product defect:
Wiring I checked myself (not from the review output): new strategy sits after The T0a assertion ( HOLD, 0 confirmed product defects. CI on this base currently fails Fast Quality Gates / some unit shards on files this PR does not touch — same fingerprints as the other open PRs on |
|
Head is now This commit keeps session stickiness on. New conversations draw leftover / (1 + in-flight). An existing conversation stays on its account until that account is empty, then rebinds. The in-flight slot is reserved on the post-stickiness Technical review of the three logic files on this SHA ran four cycles and did not converge (fingerprints kept changing). I stopped there and checked the surviving claims against the tree:
Product defects from this SHA: 0. Tests: 27/27 on the new file, 110/110 on the sibling combo suites. I did not change production |
Cherry-pick of diegosouzapw#12789 onto the deploy recut conflicted here. quota-weighted needs to skip the 99% exhaustion filter so the B pool can still draw leftover accounts. Accept the optional 5th argument; default keeps extra-usage cutoff behaviour. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Cherry-pick of diegosouzapw#12789 onto the deploy recut conflicted here. quota-weighted needs to skip the 99% exhaustion filter so the B pool can still draw leftover accounts. Accept the optional 5th argument; default keeps extra-usage cutoff behaviour. Signed-off-by: Minxi Hou <houminxi@gmail.com>
793ce63 to
7a58217
Compare
|
Looked at No code change from this pass. The TOCTOU fix stands. The lock is the two-pipeline race test: it fails if the same-tick Notes that came up, and why they do not need a patch:
|
Soft-floor routing needs remaining% from the same session/weekly windows as scoreResetAwareQuota. Missing snapshots stay at 100 so they are not treated as empty; limitReached is 0. Float dust from 1-percentUsed is rounded to 6 decimals so 30% is 30, not 30.000000000000004. Signed-off-by: Minxi Hou <houminxi@gmail.com>
quota-weighted needs remaining <=1% in the B pool. The dashboard kick (DEFAULT_QUOTA_THRESHOLD_PERCENT=99) would empty that pool for agy. Fifth arg skipExhaustionFilter defaults off so reset-aware / headroom and connection-aware expansion stay the same. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Hard-empty (remaining 0 / limitReached) is dropped. Remaining above the floor is pool A; (0, floor] is B and only used when A is empty. First pick is a weighted draw over reset-aware scores; the unused selected pool plus B form the failover tail. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Adds the public strategy value, HANDLED entry, applyStrategyOrdering branch, Zod floor, default 1%, prompt-cache first-slot protection, i18n keys in all 42 locales, and dashboard fallback copy. page.tsx own-growth 5018->5032 is the two FALLBACK maps. Signed-off-by: Minxi Hou <houminxi@gmail.com>
T0a/T0b now fail if quota-weighted is added to CONNECTION_AWARE_EXPANSION_GROUP_B. That strategy expands itself; sitting in group B would double-expand when the opt-in switch is on. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Number(null) and Number("") both coerce to 0, so a combo whose config
carried the key as null, or a dashboard field left empty, silently turned
the soft floor off. That put a 0.5% account in the first slot ahead of a
healthy one, which is the exact case the floor exists to prevent.
Only a real number, or a non-empty numeric string, may move the floor now.
Everything else takes the documented default of 1.
getResetAwareRemainingPercent had copied the five lines that turn a quota snapshot into per-window remaining fractions from scoreResetAwareQuota. Two copies of the same fallback rules would eventually drift, and the strategy depends on both agreeing: an account is placed in a pool by its leftover and ranked inside that pool by its score. The weighted draw helper also loses its ForTests suffix. It sits on the production hot path and was only ever exported so the half-open boundary could be pinned directly; the name should say what it is.
… tests Move the fragment out of the illegal changelog.d/feat/ directory into features/12789-quota-weighted-routing.md so merge-integrity accepts it, and list tests/unit/combo/quota-weighted-strategy.test.ts in stryker.conf.json tap.testFiles so FQG mutation counts its kills. Does not register reset-aware-request-scope-12600 or rewrite fixes/reset-aware-model-family.md; those are tip inherited. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Keep session stickiness on. New conversations draw leftover / (1+inflight); an existing conversation stays on its account until that account is empty. Reserve the post-stickiness [0] so a pin cannot charge the wrong account. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Cherry-pick onto the deploy recut conflicted with diegosouzapw#12803 at the isQuotaExhaustedForRequest call. Keep skipExhaustionFilter and pass connection.providerSpecificData as an optional 4th argument so a later extra-usage PR can honor blockExtraUsage=false without rewriting this site. The argument is unused on this branch. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Cover the onmi-gemini3.6 production shape that the original suite missed: disableSessionStickiness re-draws past a leftover sticky pin, ten equal agy accounts dilute the first pick by in-flight, three hard-empty of ten never win, and a Claude-empty weekly window does not drop a Gemini account after convertUsageToQuotaInfo. Injection: dropping the inflight divisor made the ten-account draw fail as expected. Production code is unchanged. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Two in-process pipelines could both see inflight=0 because the orderer drew, then yielded through stickiness/cache, and only then reserved. Move incrementInflight into the same synchronous turn as the pick. applyStrategyOrdering hands back the release; the pipeline transfers the slot if stickiness later moves [0]. Tests: concurrent Promise.all pipelines, orderer half-open boundary, floor=0 leftover as first pick, p2c still keeps hard-empty accounts, family:gemini fetch scope. Injection: dropping the same-turn increment made the race test fail (both pipelines landed on the same idle account). Signed-off-by: Minxi Hou <houminxi@gmail.com>
quota-share reserved inside selectQuotaShareTarget. Stickiness / prompt-cache could then promote a different account while the drawn connection kept the slot. Mirror the quota-weighted transfer. Empty-id fallback stays quota-weighted only so a no-op quota-share release is never double-counted. Signed-off-by: Minxi Hou <houminxi@gmail.com>
8cef05c to
75be3b0
Compare
Cherry-pick of diegosouzapw#12789 onto the deploy recut conflicted here. quota-weighted needs to skip the 99% exhaustion filter so the B pool can still draw leftover accounts. Accept the optional 5th argument; default keeps extra-usage cutoff behaviour. Signed-off-by: Minxi Hou <houminxi@gmail.com>
|
Rebased onto
|
…ibution (diegosouzapw#12772) * chore(ci): guard commit identity in pre-commit to stop author misattribution Two windows of commits in this checkout were signed with the wrong identity, both caused by an identity override left behind by an automated session: 2026-08-13..26 (name "Xiangzhe" + @backryun's e-mail, 237 commits) and 2026-08-29..09-02 (name "Markus Hartung" + the maintainer's e-mail, 59 commits). The .mailmap repairs the record after the fact; this gate stops the next window. The gate is opt-in per machine via omniroute.expectedName / expectedEmail — with no config it exits 0, so contributors who clone the repo are never affected. It blocks three things: a committer that is not this machine's identity (which is what BOTH windows looked like — in August neither the name nor the e-mail was the maintainer's, so checking only their e-mail would have missed it), an author carrying the maintainer's e-mail under someone else's name, and any address listed in omniroute.legacyEmail. Crediting a contributor with `git commit --author="Name <their@email>"` keeps working, since the rule targets the committer and the maintainer's own address. * test(ci): isolate the identity gate's test from the ambient git config The "stays inert when the machine has not opted in" case read the real global config, so on a machine that HAS opted in (omniroute.expectedEmail set — the maintainer's own boxes, where this gate matters most) the gate correctly refused a synthetic contributor identity and the test failed. It only passed on a clean CI runner. Neutralising GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM makes the opt-in state come solely from what the test injects, so the suite is deterministic on both an opted-in and a clean machine.
…2770) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. O `65536` era o 16º posicional de um helper com 15 parâmetros — `TS2554` vivo no tip (`open-sse/executors/glm.ts:244`, confirmado aqui antes do board). O teste de guarda de aridade é o que impede a reincidência: ele checa a assinatura do helper e o call site, não o comportamento, que é exatamente onde o erro morava. Obrigado por isolar isso do diegosouzapw#12711 em vez de deixar o `glm.ts` viajar junto com pin/combo-split/moonshot.
…iegosouzapw#12711) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. Além do bug do toast, esta PR foi a que derrubou os três base-reds vivos do tip: o fragmento `changelog.d/fixes/reset-aware-model-family.md` sem o `- ` inicial, o registro do `tests/unit/reset-aware-request-scope-12600.test.ts` no `stryker.conf.json` e o `TS2554` do glm. O `check-changelog-integrity` voltou a passar aqui por causa dela. O diagnóstico do MouseEvent é o que dá o valor: `onConfirm` chegava como handler de clique nativo e `handleBatchDeleteConfirm` tratava qualquer primeiro argumento truthy como callback. O cinto (`typeof`) e o suspensório (o wrap no ConfirmModal) juntos estão certos — só um dos dois deixaria a porta aberta para o próximo caller.
) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. O `nodeMap` lido do closure de `RuntimePageClient` por uma função de nível de módulo é uma bomba-relógio silenciosa: só explode quando um monitor entra em error/exhausted/alerting, e o teste existente só alimentava listas vazias. Tirar o arquivo do exclude do vitest vale tanto quanto o fix — confirmei aqui que `tests/unit/ui/runtime-page-client.test.tsx` agora roda na `test:vitest:ui` e passa. A anotação sobre o "内部服务器错误" ser o catálogo RSC da página, e não o crash, poupou o próximo a caçar fantasma.
…diegosouzapw#12733) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. Available 0% com Voucher 100% e Cash 100% não é estado de carteira que exista — foi o sinal certo para puxar o fio. Tratar leftover como booleano só para Available e cravar 100% nos outros dois buckets é o tipo de defeito que passa despercebido enquanto a conta tem saldo. Os dois testes cobrem os dois lados: o produtor e o caminho até `getQuotaRemainingPercentage` com `isCredits` + CNY.
…egosouzapw#12767) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. A separação entre o que é do service worker e o que é do Caddy está certa e é o que torna a PR mergeável: o `respondWith` em navegação é defeito nosso, o `Alt-Svc` mentindo h3 é config de proxy reverso e não tem o que fazer aqui. O `/dashboardfoo` casando com `startsWith("/dashboard")` é um achado à parte, e o bump de cache v2→v3 é o que faz o worker antigo sair do ar nos clientes que já estão presos.
…gets (diegosouzapw#12475) (diegosouzapw#12926) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. A busca do sufixo mais longo primeiro (`-xhigh` antes de `-high`) é o detalhe que faz a herança funcionar em vez de quase-funcionar. Manter `getResolvedModelContextOverride` fora do escopo, com o teste existente registrando que aquele caminho continua sem herança, deixa a fronteira explícita.
… member (diegosouzapw#12899) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. Regressão de v3.8.50 vinda do diegosouzapw#9057, com o sintoma mais enganoso possível: `attempted: 0`. A política já tinha admitido o combo e a checagem era refeita em cada membro interno. Manter o filtro por prefixo de provider e o `disableNonPublicModels` intactos é o que impede o short-circuit de virar um buraco na allow-list.
…ts (diegosouzapw#12805) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. A decodificação dos campos aninhados 10/20/30 do `GetRemainingResets` ao vivo (último commit) é o que separa isto de um palpite sobre o formato do frame. Mostrar zero em vez de esconder a linha é a escolha certa: crédito zerado é informação, ausência de linha é ambiguidade.
…apw#12803) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. `blockExtraUsage: false` significa "pode usar crédito extra", nunca "esconda a conta antes de despachar" — a conta saía da rota justamente quando o crédito extra existia para ser usado. Os 32/32 cobrem os quatro pontos onde a mesma decisão era tomada, e revalidei após o merge da base (32/32 de novo). Nota de integração: o seu `isQuotaExhaustedForRequest` colidiu com o placeholder `_providerSpecificData` do diegosouzapw#12789 na worktree combinada. Ficou a sua implementação, que é a que de fato usa o parâmetro. A base foi mergeada na branch para resolver o `file-size-baseline.json` (aditivo, JSON revalidado).
…iegosouzapw#12697) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. Após o merge da base nesta branch, os 11/11 do `combo-pin-implicit-allowlist` foram revalidados. A distinção entre pin de step de combo e pin forçado por header (`x-omniroute-connection`) é o que salva a PR de virar uma restrição ampla demais — o header continua permitindo fallback para conexões irmãs, o step não. Apontar que o `a11930ec4` para a rotação dentro do `handleSingleModel` mas não popula `allowedConnectionIds` no resolve foi a peça que explicou por que os dois são complementares e não redundantes. Sem isso a PR pareceria duplicar um gate que já existia.
…atalog (diegosouzapw#12597) (diegosouzapw#12934) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. A assimetria era exatamente o defeito: o REST do picker já mesclava `customModels`, o despacho não, e o operador via o modelo na tela e tomava 400 na inferência. O overlay só de campos definidos é o detalhe que impede uma escrita esparsa do picker de apagar metadata de capacidade que veio do sync. Nota de integração: este arquivo colidiu com o diegosouzapw#12866, que extraiu o mesmo bloco para `loadConnectionCatalog` e uniu os catálogos irmãos agy/antigravity. Integrei os dois na worktree combinada — a união de irmãos primeiro, o `unionCustomModels` por cima — e a resolução vai junto no merge do diegosouzapw#12866.
…iegosouzapw#12866) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. O ponto que sustenta a PR é o `models.dev` virar overlay de preço em vez de fonte de catálogo. Um catálogo estático que sobrevive à conta já ter listado ids mais novos é o tipo de defeito que só aparece quando o modelo novo é justamente o que se quer usar. Nota de integração: `activeSyncedCatalog.ts` colidiu com o diegosouzapw#12934 (união dos `customModels` do picker no catálogo de despacho). Como você extraiu o bloco original para `loadConnectionCatalog`, os dois se compõem: a união dos irmãos agy/antigravity primeiro, o `unionCustomModels` por cima. Revalidei com `custom-models-live-catalog-12597`, `live-model-catalog-reconciliation-8926`, `sync-models-degraded-cached-catalog-9683`, `models-dev-catalog-read-gate`, `discovery-class`, `reactive-model-sync` e `l1-oauth-autosync-default` juntos — 48/48 — mais typecheck:core limpo. Sobre o `autoSync` padrão em Claude/Codex/Copilot com scheduler de 6h: passei isso pelo dono antes de mergear e a decisão foi manter como está.
c1b34db
into
diegosouzapw:release/v3.8.51
Contadores de docs fora de sincronia com o código, aprovado pelo dono em chat por tocar `AGENTS.md` e `skills/cli-tunnel/SKILL.md` (Hard Rule — superfície de instrução de agente). Nenhuma instrução mudou. `Docs Gates` acusava 6 drifts STRICT. Dois vieram da minha leva de 16 PRs: migrations 169 → **171** (#12707 trouxe a 173, #12867 a 174) e estratégias de roteamento 19 → **20** (#12789 registrou a `quota-weighted`). Contei os arquivos em vez de confiar na memória: `ls src/lib/db/migrations/*.sql | wc -l` → 171. Os 41 mirrors de `docs/i18n/*/llm.txt` foram regenerados com `scripts/i18n/sync-llm-mirrors.mjs` — o gate exige cópia exata da raiz. O outro braço, `check:agent-skills-sync` acusando `GENERATED: + cli-tunnel`, era herdado (o corpo do #12866 já o registrava). O `SKILL.md` commitado documentava `tunnel create [type]`, um argumento que a CLI **não aceita** — conferido em `bin/cli/commands/tunnel.mjs:21`, que declara `.command("create")` puro. Saída do gerador, não escrita à mão. | gate | antes | depois | |---|---|---| | `check:docs-counts` | 6 drifts STRICT | **0** | | `check:docs-sync` | FAIL — 41 mirrors divergentes | **PASS** | | `check:agent-skills-sync` | `+ cli-tunnel` | **UNCHANGED: 46 skills** |
…apw#12803) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. `blockExtraUsage: false` significa "pode usar crédito extra", nunca "esconda a conta antes de despachar" — a conta saía da rota justamente quando o crédito extra existia para ser usado. Os 32/32 cobrem os quatro pontos onde a mesma decisão era tomada, e revalidei após o merge da base (32/32 de novo). Nota de integração: o seu `isQuotaExhaustedForRequest` colidiu com o placeholder `_providerSpecificData` do diegosouzapw#12789 na worktree combinada. Ficou a sua implementação, que é a que de fato usa o parâmetro. A base foi mergeada na branch para resolver o `file-size-baseline.json` (aditivo, JSON revalidado).
…ftover (diegosouzapw#12789) Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. Reservar o sorteio antes do próximo `await` (`2cf74acc`) é a parte não-óbvia e a que mais importa: sem isso dois pipelines no mesmo processo observam `inflight=0` na mesma conta e convergem para ela. O comentário no código explica isso melhor do que o commit message. **Um ajuste meu na sua branch.** O `tests/unit/combo/quota-weighted-strategy.test.ts` era intermitente — falhava em cerca de 1 a cada 5 execuções, alternando entre `A/B isolation: 7 hard-empty…` e `floor=0 puts 0.5% in the main pool`, sempre com dois pares de mesma faixa trocando de posição. A causa é o helper de fixture: ```ts const iso = (ms = 86_400_000) => new Date(Date.now() + ms).toISOString(); ``` Como `iso()` é chamado a cada invocação do fetcher, dois peers que deveriam empatar recebiam `resetAt` com um milissegundo de diferença sempre que o relógio virava entre as duas chamadas. Pressão de reset entra no score, então esse epsilon quebrava o empate e `sortByScoreThenIndex` nunca chegava ao fallback por índice de inserção. Fixei a base do relógio uma vez só (`CLOCK_BASE`). Nenhuma asserção foi tocada — as garantias de ordem, tamanho e exclusão continuam idênticas. 10/10 execuções verdes depois, e mais 6/6 após o merge da base nesta branch. Também mergeei a base para resolver `file-size-baseline.json` (aditivo) e `src/domain/quotaCache.ts`, onde o seu placeholder `_providerSpecificData` cedeu lugar à implementação do diegosouzapw#12803, que usa o parâmetro de fato.
…12970) Contadores de docs fora de sincronia com o código, aprovado pelo dono em chat por tocar `AGENTS.md` e `skills/cli-tunnel/SKILL.md` (Hard Rule — superfície de instrução de agente). Nenhuma instrução mudou. `Docs Gates` acusava 6 drifts STRICT. Dois vieram da minha leva de 16 PRs: migrations 169 → **171** (diegosouzapw#12707 trouxe a 173, diegosouzapw#12867 a 174) e estratégias de roteamento 19 → **20** (diegosouzapw#12789 registrou a `quota-weighted`). Contei os arquivos em vez de confiar na memória: `ls src/lib/db/migrations/*.sql | wc -l` → 171. Os 41 mirrors de `docs/i18n/*/llm.txt` foram regenerados com `scripts/i18n/sync-llm-mirrors.mjs` — o gate exige cópia exata da raiz. O outro braço, `check:agent-skills-sync` acusando `GENERATED: + cli-tunnel`, era herdado (o corpo do diegosouzapw#12866 já o registrava). O `SKILL.md` commitado documentava `tunnel create [type]`, um argumento que a CLI **não aceita** — conferido em `bin/cli/commands/tunnel.mjs:21`, que declara `.command("create")` puro. Saída do gerador, não escrita à mão. | gate | antes | depois | |---|---|---| | `check:docs-counts` | 6 drifts STRICT | **0** | | `check:docs-sync` | FAIL — 41 mirrors divergentes | **PASS** | | `check:agent-skills-sync` | `+ cli-tunnel` | **UNCHANGED: 46 skills** |
Summary
Adds combo strategy
quota-weighted.Same-model account pools (Antigravity Gemini is the case that bit us) currently have two bad fits:
p2cscores by model latency, so ten identicalmodelStrrows look the same and empty accounts stay in the draw;reset-awarealways takes the current snapshot leader, so concurrent workers pile onto one connection until the next refresh. This strategy drops accounts that are actually empty, then draws the first target in proportion to reset-aware leftover.p2c/reset-aware/headroom/quota-shareare unchanged. New enum value, dashboard can pick it.Changes
getResetAwareRemainingPercentnext to the existing reset-aware score.expandTargetsByQuotaAwareConnectionstakes an optionalskipExhaustionFilter. The 99% hard filter would wipe the 1% soft-floor pool before the orderer can use it.orderTargetsByQuotaWeighted: circuit-open and remaining=0 stay out; draw from pool A (remaining > floor, default 1%); if A is empty, draw from B; tail is leftover descending. All-zero weights fall back to uniform.quotaWeightedFloorPercent), dispatch, prompt-cache protect-first (otherwise the cache restack moves the weighted first target), dashboard fallback copy, 42 locales.quota-weightedis not in group-B connection-aware expansion. It expands itself; putting it in B double-expands.Does not flip any production combo. After merge, set the combo to
quota-weightedanddisableSessionStickiness=true. Leave stickiness on and the first hit is pinned for the rest of the session, which is the opposite of a spread. The dashboard tips say this; the strategy does not change the global default.Test plan
tests/unit/combo/quota-weighted-strategy.test.ts(weighted draw, A/B split, skip-exhaustion expand, uniform fallback)tests/unit/combo/connection-aware-expansion.test.tsT0a/T0b:quota-weightedstays out of group Bquota-weightedtoCONNECTION_AWARE_EXPANSION_GROUP→ T0a+T0b fail; restore → passrelease/v3.8.51). Not fixing those here.Notes
Base:
release/v3.8.51@9d1a896c6.