refactor(combo): split executeTarget into gates, attempt, and loop - #12746
diegosouzapw merged 22 commits into
Conversation
|
Technical review notes for the follow-up LOCAL The UNCERTAIN is Earlier extract/hygiene reviews on this branch also HOLD with 0 CONFIRMED. Lift-as-is findings that praised dead-code removal were not defects. CI on Contributor only — not merging. |
83ff97a to
5a5ee9b
Compare
|
Rebased onto range-diff every unique commit Own tests re-run on the new base.
|
5a5ee9b to
ec88a63
Compare
Types only. Characterization test watched ERR_MODULE_NOT_FOUND then PASS. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Copy the 10 skip gates (breaker through admission) into executeTargetGates.ts. combo.ts still owns executeTarget until the attempt/loop extract. Circuit-open counters and per-target admission join AttemptLoopState/Deps. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Task 2 extract keeps combo.ts executeTarget behavior. Comments pin the four contracts reviewers treated as new defects. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Lift-as-is retry loop from combo.ts:1533-2616 into executeTargetAttempt.ts. Pure classify predicates (homogeneous remainder, input-bound abort, body-specific 400) live in executeTargetClassify.ts. Pin/LKGP go through deps so injection can catch a missed forward. combo.ts not wired yet (Task 4). Attempt file frozen at 1205 after classify extract (54 LOC). Signed-off-by: Minxi Hou <houminxi@gmail.com>
Extract dispatchWithCooldownRetry into comboAttemptLoop.ts. handleComboChatInner now builds AttemptLoopState/Deps and calls the extracted loop; executeTarget is evaluateGates then executeAttempt. diegosouzapw#11804 finally stays on the loop. Injection: noop deps.releaseStickyPinOnFailure at executeTargetAttempt.ts:395 fails quality-rejected 200 (released==null); restore 7/7 green. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Attempt budget already lives on state.globalAttempts (executeTargetAttempt).
The extra.{current} box, unused delay locals in handleComboChatInner, unused
clearComboFailureTracking import, and unused timeoutResolve were lift leftovers.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Barrel still imported lockout/handoff/trace helpers that now live in the attempt/gates leaves. Unused HandleRoundRobinCombo args renamed in destructure so the type stays intact. Signed-off-by: Minxi Hou <houminxi@gmail.com>
ec88a63 to
152bf96
Compare
|
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á.
…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.
6b587d0
into
diegosouzapw:release/v3.8.51
…12811) 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. **Sobre a reconstrução da branch.** Esta PR continha os 7 commits do #12746 mais os 3 do round-robin. O dono escolheu mergear os dois em sequência em vez de fechar um como subsumido, então depois que o squash do #12746 entrou eu reconstruí esta branch: cherry-pick de `05059880`, `54168238` e `ddd6bcbf` sobre o tip novo, e force-push. Autoria preservada — os três commits continuam seus (`Minxi Hou <houminxi@gmail.com>`), verificado com `git log --format=%an` antes do push. A PR foi de +4167/−3187 em 14 arquivos para +1281/−1182 em 5, que é o delta real do round-robin. O `05059880` ("guard round-robin extract before the lift") é o commit que faz esse tipo de extract ser revisável: sem um teste que fixe o contrato antes do movimento, mover 1198 linhas é indistinguível de reescrever 1198 linhas. Revalidei sobre o tip reconstruído: `round-robin-combo`, `combo-attempt-loop`, `execute-target-attempt`, `execute-target-gates` e `combo-loop-safety-timer-leak-11804` — 24/24 — com typecheck:core limpo e o cap de arquivo OK.
…12884) Um `ALL_TARGETS_SKIPPED` 503 que não diz qual janela esgotou é opaco justamente no momento em que o operador mais precisa saber. Alinhar os rótulos de janela AUTH com os da API de uso fecha a outra metade: dois nomes para a mesma coisa fazem o dashboard e o erro parecerem discordar. Revalidei sobre o tip: **6/6**, typecheck:core limpo, check-file-size OK. **Dois consertos meus na sua branch.** 1. `typecheck:core` falhava com `TS2345` em `comboAttemptLoop.ts` (linhas 130 e 416): o `QuotaSkipTarget` declarava `connectionId?: string`, mas o `ResolvedComboTarget` carrega `string | null` para alvo não-pinado. Alarguei para `string | null` no tipo de diagnóstico em vez de estreitar o call site — o módulo só **lê** o campo e a linha 29 já narrowa com `typeof === "string"`, então null não custa nada ali. Isso apareceu porque o `comboAttemptLoop` mudou de forma no #12746/#12811, mergeados nesta mesma campanha depois que você cortou a branch. 2. O `roundRobinCombo.ts` foi de 1198 para 1205 e cruzou o teto de 1200 para arquivo novo. Congelei com justificativa: o arquivo já nasceu em 1198 quando o #12811 o levantou de dentro do `combo.ts`, e os diagnósticos em si vivem no `quotaSkipDiagnostics.ts`, sob o cap. Registrei que a próxima extração natural é o corpo do attempt loop, mas que ele acabou de ser movido e deve assentar antes de ser cortado de novo.
… the whole fallback chain A malformed/unsupported request shape (e.g. an incompatible tool-call history for a provider's translation layer) fails the SAME way against every fallback target, since it is a property of the request, not of any one provider. Detect 3 consecutive targets failing with the identical model-shape error (same kind, status, and message) and stop retrying -- both the remaining targets in this pass and the whole-set retry -- instead of burning through all MAX_GLOBAL_ATTEMPTS fallbacks identically. Rebased onto release/v3.8.51: the original combo.ts main loop this was written against has since been split into comboAttemptLoop.ts (gates/ attempt/loop extraction, diegosouzapw#12746), so the fix now lives in the equivalent per-target loop and set-retry guard there, and the terminal response reuses the loop's own already-generalized resolveComboTerminalStatus()/formatComboOutcomes() exhaustion path (diegosouzapw#10501) instead of a separate response branch.
…cted fences Both reds were inherited from release/v3.8.51 — every production file involved is byte-identical to the base tip. Three PRs merged 2026-09-07 moved or added call sites without updating the golden inventory: - #12867 (d6f3150) extracted the codex 429 / antigravity 422 account rotation out of chatCore.ts into chatCore/providerExecutionPipeline.ts. The managed-lease fence was NOT removed: it crosses the seam as `policy.allowAccountRotation`, still derived from `!managedLease` on both the streaming and the non-streaming leg. The antigravity branch, which had no lease fence at all before the extract, is now gated by the same flag. The two credential-resolution sites moved with it and are called off the injected `connection` context, so countCalls() now also sees that property-access shape — otherwise an extract-to-a-seam refactor would silently drop a credential site out of the inventory. - #12746 (6b587d0) lifted combo.ts's persisted-cooldown connection read into combo/executeTargetGates.ts byte-identically (same site, renamed). - #12805 (c042a51) added grok-cli reset credits: grokResetCredits.ts already carries isConnectionUnavailableToAuxiliaryActivity() ahead of its lookup, so it joins the auxiliary-isolation source list; the shared reset-credit route reads the connection only to pick a handler (class C). Assertions are pinned tighter, not looser: the single chatCore regex is replaced by both ends of the split fence, and the two rotation-policy sites are counted exactly rather than merely detected.
… the whole fallback chain A malformed/unsupported request shape (e.g. an incompatible tool-call history for a provider's translation layer) fails the SAME way against every fallback target, since it is a property of the request, not of any one provider. Detect 3 consecutive targets failing with the identical model-shape error (same kind, status, and message) and stop retrying -- both the remaining targets in this pass and the whole-set retry -- instead of burning through all MAX_GLOBAL_ATTEMPTS fallbacks identically. Rebased onto release/v3.8.51: the original combo.ts main loop this was written against has since been split into comboAttemptLoop.ts (gates/ attempt/loop extraction, diegosouzapw#12746), so the fix now lives in the equivalent per-target loop and set-retry guard there, and the terminal response reuses the loop's own already-generalized resolveComboTerminalStatus()/formatComboOutcomes() exhaustion path (diegosouzapw#10501) instead of a separate response branch.
…iegosouzapw#12746) 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. Guardar os contratos com testes ANTES de levantar o bloco (`05059880` no irmão, e o `287658f5` marcando os `executeTargetGates` como lift-as-is) é o que torna um refactor deste tamanho auditável. Sem essa ordem, um extract de 3 mil linhas é indistinguível de uma reescrita. Revalidei depois do merge da base: `combo-attempt-loop`, `execute-target-attempt`, `execute-target-gates` e `combo-loop-safety-timer-leak-11804` — 20/20 — mais typecheck:core limpo e o cap de arquivo OK. O diegosouzapw#12811 entra na sequência logo em seguida, com os três commits do round-robin sobre este.
…iegosouzapw#12811) 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. **Sobre a reconstrução da branch.** Esta PR continha os 7 commits do diegosouzapw#12746 mais os 3 do round-robin. O dono escolheu mergear os dois em sequência em vez de fechar um como subsumido, então depois que o squash do diegosouzapw#12746 entrou eu reconstruí esta branch: cherry-pick de `05059880`, `54168238` e `ddd6bcbf` sobre o tip novo, e force-push. Autoria preservada — os três commits continuam seus (`Minxi Hou <houminxi@gmail.com>`), verificado com `git log --format=%an` antes do push. A PR foi de +4167/−3187 em 14 arquivos para +1281/−1182 em 5, que é o delta real do round-robin. O `05059880` ("guard round-robin extract before the lift") é o commit que faz esse tipo de extract ser revisável: sem um teste que fixe o contrato antes do movimento, mover 1198 linhas é indistinguível de reescrever 1198 linhas. Revalidei sobre o tip reconstruído: `round-robin-combo`, `combo-attempt-loop`, `execute-target-attempt`, `execute-target-gates` e `combo-loop-safety-timer-leak-11804` — 24/24 — com typecheck:core limpo e o cap de arquivo OK.
…iegosouzapw#12884) Um `ALL_TARGETS_SKIPPED` 503 que não diz qual janela esgotou é opaco justamente no momento em que o operador mais precisa saber. Alinhar os rótulos de janela AUTH com os da API de uso fecha a outra metade: dois nomes para a mesma coisa fazem o dashboard e o erro parecerem discordar. Revalidei sobre o tip: **6/6**, typecheck:core limpo, check-file-size OK. **Dois consertos meus na sua branch.** 1. `typecheck:core` falhava com `TS2345` em `comboAttemptLoop.ts` (linhas 130 e 416): o `QuotaSkipTarget` declarava `connectionId?: string`, mas o `ResolvedComboTarget` carrega `string | null` para alvo não-pinado. Alarguei para `string | null` no tipo de diagnóstico em vez de estreitar o call site — o módulo só **lê** o campo e a linha 29 já narrowa com `typeof === "string"`, então null não custa nada ali. Isso apareceu porque o `comboAttemptLoop` mudou de forma no diegosouzapw#12746/diegosouzapw#12811, mergeados nesta mesma campanha depois que você cortou a branch. 2. O `roundRobinCombo.ts` foi de 1198 para 1205 e cruzou o teto de 1200 para arquivo novo. Congelei com justificativa: o arquivo já nasceu em 1198 quando o diegosouzapw#12811 o levantou de dentro do `combo.ts`, e os diagnósticos em si vivem no `quotaSkipDiagnostics.ts`, sob o cap. Registrei que a próxima extração natural é o corpo do attempt loop, mas que ele acabou de ser movido e deve assentar antes de ser cortado de novo.
Summary
handleComboChatInner's 1327-lineexecuteTargetis now three modules: pre-dispatch skip gates, the retry/side-effect loop, and the setTry/safety-timer dispatcher.combo.tsgoes from 4080 to 2234 lines (split("\n").length). Round-robin stays incombo.ts.Why
combo.tssat on its file-size freeze (4080). The leftover bulk wasexecuteTarget, not more types. ROADMAP 3.8.52 lists this split under PREPARE.What moved
open-sse/services/combo/attemptLoopTypes.tsAttemptLoopState/AttemptLoopDepsopen-sse/services/combo/executeTargetGates.tsopen-sse/services/combo/executeTargetClassify.tsopen-sse/services/combo/executeTargetAttempt.tsopen-sse/services/combo/comboAttemptLoop.tshandleComboChatInnerbuildsAttemptLoopState/AttemptLoopDepsand callsdispatchWithCooldownRetry. Control flow is copied as-is: pin release, LKGP, quota cutoff, 499/400/quality, loop-safetyfinally, cooldown-wait redispatch.Tests
Tip
b9b24fbfdonupstream/release/v3.8.51@c3945a724.107/107 pass.
Injection (commit
73a1c6c5f):releaseStickyPinOnFailureatexecuteTargetAttempt.ts:395replaced with() => {}. Quality-rejected 200 then failed withreleased==null(2 failures). Restore: 7/7 green.node scripts/check/check-file-size.mjs --base-ref upstream/release/v3.8.51OK. New leafexecuteTargetAttempt.tsfrozen at 1205: remaining growth is I/O plus side effects inside the retry loop; splitting that mid-request would change behavior.Not in this PR
handleRoundRobinCombo(~1055 lines still incombo.ts). It copies skip/classify/pin. A later PR can call these functions if the signatures fit. Tests here do not change RR behavior.chatCore.tsBase-red inherited
release/v3.8.51currently red). Failures that match that issue are not from this split.