fix: clear eight base-reds from the 2026-09-24 merge wave — abort-listener leak, pre-content retry, MCP bundle deadlock, zh-TW glossary, sidebar keys, flush-empty-retry, proxy-status and pack-policy tests (#14547) - #14820
Merged
Merged
Conversation
…no live body Root cause: #14342 changed executeWithUpstreamStartTimeout to keep the client-signal `abortListener` (the link that aborts combinedController) whenever the start-timeout race settled successfully, so a client abort after headers could still reach the upstream fetch. It kept the link for every resolved result, including results that carry no streaming body, so one listener stayed on the client signal after the race settled. That broke the #12406 guard ("every listener registered for the race must be removed once it settles", 1 !== 0). Fix: keep the link only when the settled result (a Response or the executor's `{ response }` wrapper) still has an unconsumed body that will stream on the combined signal. Otherwise remove it in the finally, as for failed attempts. The abortPromise listener is still always removed. The #14342 post-headers propagation is unchanged for streaming responses. Tests: the #12406 guard is green again; two new cases pin both sides (a body-less Response releases the link; a streaming body keeps exactly one link, propagates the abort, and it self-removes on abort). Refs #14547
… reasons #13630 gated the same-target retry after a pre-content streaming upstream error on an exact match: handlePreContentStreamRetry() (open-sse/services/combo/executeTargetClassify.ts:35) compared quality.reason !== "streaming upstream error". #14314 (b3867e4) then intentionally appended the upstream detail to that reason in validateResponseQuality() ("streaming upstream error: <detail>") so the real Anthropic rejection reaches the call log. Every detailed reason failed the exact match, so protected (fallbackOnlyOnQuotaExhaustion) and native-pinned targets stopped retrying once and failed instead. validateQuality.ts now owns STREAMING_UPSTREAM_ERROR_REASON, builds the reason in one helper and exports isStreamingUpstreamErrorReason(), which the retry gate uses (bare or "<base>: <detail>", nothing else). The two reason assertions in the #13630 test move to the exact new text #14314 documents, and a classifier test pins both forms plus near-misses. Refs #14547
#14014 added combos.advancedHelp.connectionAwareExpansion to zh-TW with the retired rendering 供應商 (twice) for the "provider" concept, while the zh-TW glossary's canonical term is 提供者. That broke two cases of tests/unit/i18n-glossary-consistency-check.test.ts ("real zh-TW.json + real zh-TW glossary pass the gate" and "zh-TW.json is free of the retired renderings"). Replace both occurrences with 提供者 in that one value; the rest of the sentence, the other keys #14014 added and the glossary are untouched. Refs #14547
… the loopback exemption The #13580 guard "a fetch still running when the dispatch returns does not change the status" started a background fetch to the refused port 127.0.0.1:1 and assumed it would still be in flight when the dispatch settled. That only held because loopback targets used to go through the direct undici bound-and-replay path: ECONNREFUSED on attempt 0, a RETRY_BACKOFF_MS wait, attempt 1, then the native fallback. #14311 intentionally sends loopback targets straight to native fetch (open-sse/utils/proxyFetch.ts, the isLoopbackTarget early return), so the refused call now rejects within about a millisecond, while the dispatch is still reading the provider body. withUpstreamStatusCapture then does what it documents for a call that throws during an active dispatch: it clears upstreamStatus, and the test read undefined instead of 429. A probe showed ":1 rejected dispatching=true" before "dispatch settled". The capture code is unchanged and correct; the test's timing assumption broke. The failing background call is now a delayed connection drop on the test server (drop=1, delay=50), which still fails after the dispatch settles, as the test intends. The 429 assertion is unchanged. A mutation that ignores the settled dispatch flag still fails the test. Refs #14547
Root cause: #14555 added `import { isQuotaHealthy } from "@/domain/quotaCache"` to open-sse/services/quotaPreflight.ts. quotaCache already imports open-sse/services/usage.ts -> usage/openrouter.ts -> openrouterQuotaFetcher.ts -> quotaPreflight.ts, so the new edge closed an ESM cycle. In the esbuild MCP bundle every module is an async __esm initializer; quotaPreflight's init awaited quotaCache's still-pending init promise, the chain never settled and `import(server.js)` exited 13 with "Detected unsettled top-level await" (tests/unit/build/mcp-bundle-startup.test.ts). Fix: move the shared globalThis state (#8065), the entry types, EXHAUSTED_MAX_PARK_MS and the healthy-override helpers into a runtime import-free leaf, src/domain/quotaCacheState.ts. quotaCache imports and re-exports them (chat.ts and tests unchanged); quotaPreflight imports the leaf directly. Same `__omnirouteQuotaCacheState.healthyUntil` map, so the #14555 park cap and healthy override behave exactly as before. Refs #14547
…nance It changes no production code. Refs #14547
#14684 added a model-catalog entry to the sidebar with i18nKey modelCatalog and subtitleKey modelCatalogSubtitle, but only inline fallbacks and no en.json keys, so settings-i18n-keys ('English sidebar translations include every configured sidebar item') failed on the release tip. The English values are the fallbacks #14684 already ships; other locales fall back to them until the next i18n refresh. Refs #14547
…pass #14691 stops the bounded read at the first useful chunk and returns early-pass (verdict: pass) for any turn that already carries content or a tool call; only content-free turns are drained to the end. Four cases still fed a content chunk and expected the drained text (or the idle outcome), so they failed on the release tip (22/22 at #14691's parent, 18/4 at #14691). The intact-body and zero-idle-budget cases now use a reasoning-only turn, which is still drained whole; the stalled-with-content case expects early-pass with the same pass verdict; and the outcome test now also pins early-pass for a content turn. Production code unchanged. Refs #14547
…-exports Fast Quality Gates on this PR failed two gates: - dead-code: the quotaCacheState extraction re-exported EXHAUSTED_MAX_PARK_MS, isQuotaHealthy and unmarkQuotaHealthy from quotaCache.ts, but only markQuotaHealthy has callers outside the module (chat.ts and tests). The other three stay internal imports. - mutation-test-coverage --strict: six covering tests missing from tap.testFiles; five new (one of them from this PR's validateQuality change), the sixth was already listed. Additive only. Both gates OK locally; quota #14359 suites 29/29, open-sse typecheck clean. Refs #14547
diegosouzapw
added a commit
that referenced
this pull request
Sep 25, 2026
…count or trips the provider breaker (#14815) (#14830) Translation runs locally on the client's body before any upstream call. A translation failure (e.g. "Maximum call stack size exceeded" on one large image, #14815) returned a bare 500 that cooled the account and counted toward the provider breaker. createTranslationFailureResult() now labels the result request_translation_failed (client body unchanged); shouldSkipConnDisable() and shouldTripProviderBreakerForResult() skip it. Remaining CI reds are the release-tip base-reds tracked in #14547 and drained by #14820.
# Conflicts: # open-sse/services/combo/executeTargetClassify.ts # open-sse/services/combo/validateQuality.ts # stryker.conf.json # tests/unit/combo-responses-sse-failure-fallback.test.ts
…ync init cycle #14892 made src/lib/db/proxyLogs.ts import sanitizeTimingMs from upstreamStatusCapture.ts, which reaches usage/migrations (top-level await) through providerRequestLogging. Every DB module importing proxyLogs became async and the esbuild MCP bundle deadlocked on import again. The helper moves to a zero-import leaf; upstreamStatusCapture re-exports it.
diegosouzapw
added a commit
to riez/OmniRoute
that referenced
this pull request
Sep 29, 2026
The tip moved the quota-cache types/state into quotaCacheState.ts (diegosouzapw#14820); carry this PR's QuotaInfo.claudeQuota and QuotaCacheEntry.modelQuotas fields there (type-only import keeps the leaf free of runtime imports).
diegosouzapw
pushed a commit
that referenced
this pull request
Sep 29, 2026
Native Claude quota scope is honored end to end (claudeQuota normalizer, modelQuotas separation, effort/context-aware model matching). Maintainer rework: merged the release tip twice, reconciling markAccountUnavailable with the OAuth 401 backoff (#14917: the Claude-scope cooldown feeds resolvedCooldownMs, the backoff still overrides it, and a Claude resetAt is only used when the backoff did not apply) and carrying the claudeQuota/modelQuotas fields into the new quotaCacheState.ts leaf (#14820, type-only import). Validated on the tip: 70 related test files 683 pass; the only reds (chat-cooldown-aware-retry #6, false-terminal-401-quota #1, sse-auth #62-66) fail identically on the pure tip. typecheck:core and ESLint clean. File-size ceiling growth is reconciled in the wave follow-up. Thank you @riez!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The 2026-09-24 merge wave left eight unit tests red on
release/v3.8.51. Each one was bisected (db7092933..tip, idle .113) to the PR that broke it, and none of them is covered by another open PR:chatcore-upstream-timeouts: abort listener left on the client signalcombo-responses-sse-failure-fallback(4 cases)i18n-glossary-consistency-check(2 cases)proxyfetch-upstream-status-capturebuild/mcp-bundle-startuppack-artifact-policysettings-i18n-keys(sidebar)en.jsonkeysflush-empty-retry(4 cases)Fixes
Client-abort listener leak (#14342 vs #12406). #14342 keeps the client-abort → upstream link alive after headers so an abort still reaches a streaming body. It kept that link for every settled result, so a result with no body left one
abortlistener on the client signal. That is exactly the leak #12406 closed, which kept hedge-cancelled processes alive. The link now survives only when the settled result still has a live body (settledResultHasLiveBody). Streaming still forwards the abort, and the #14342 test stays 8/8. Two new cases pin both sides.Pre-content stream retry (#14314 vs #13630). #14314 appends the upstream detail to the quality reason (
streaming upstream error: peak capacity) for the call log. #13630's same-target retry matched the bare string exactly, so after #14314 protected and pinned targets stopped retrying once and failed instead.validateQuality.tsnow owns the base reason and exportsisStreamingUpstreamErrorReason(), which accepts the bare reason orbase: detailand nothing else.executeTargetClassify.tsuses it. The two assertions on the reason text now pin the new detailed text exactly, and a new case rejects near-misses.MCP bundle deadlock (#14555). #14555 made
quotaPreflight.tsimport@/domain/quotaCache.quotaCache → usage → usage/openrouter → openrouterQuotaFetcher → quotaPreflightcloses an ESM init cycle. In the esbuild MCP bundle every module initializes asynchronously, so the cycle deadlocks andimport(server.js)exits 13 with "unsettled top-level await". The shared quota-park state and the healthy-override helpers move to a zero-import leaf,src/domain/quotaCacheState.ts.quotaCache.tsre-exports them, so no caller changes, andquotaPreflightimports the leaf. It is the same state object, so #14555's behavior is unchanged.zh-TW glossary (#14014). One new value used
供應商twice where the zh-TW glossary term for "provider" is提供者. Only that value changed.Pack policy test (#14712). #14712 made
bin/cli/privateDataDir.mjsa required tarball path on purpose (it runs on every boot), butpack-artifact-policy.test.tsstill listed the old missing-paths set. The test now expects it (20/20).Sidebar Model catalog keys (#14684). #14684 added a
model-catalogsidebar entry withi18nKey: modelCatalog/subtitleKey: modelCatalogSubtitlebut only inline fallbacks. The two keys now exist inen.json(same text as the fallbacks), pluspt-BRandvi, which carry key-parity tests. Other locales fall back to English until the next i18n refresh; no locale sync was run. Sidebar + i18n completeness/ratio/coverage tests: 38/38.flush-empty-retry vs #14691 early-pass. #14691 stops the bounded read at the first useful chunk and returns
early-pass(verdictpass) for a turn that already carries content; only content-free turns are drained. Four cases still fed a content chunk and expected drained text oridle(22/22 at #14691's parent, 18/4 at #14691). The intact-body and zero-idle-budget cases now use a reasoning-only turn, the stalled-with-content case expectsearly-passwith the samepassverdict, and the outcome test also pinsearly-passfor a content turn. Production code unchanged; with #14691's own tests and the hook test: 39/39. Note: #14791 and #14729 also edit this test file and will need a rebase after this lands.Proxy status capture (#14311). #14311's loopback exemption makes a refused
127.0.0.1:1fail in ~1 ms instead of going through retry/backoff, so the test's "still running after dispatch" call finished during the dispatch, and the capture correctly cleared the status. The failing call is now a connection the test server drops after 50 ms. The429assertion is unchanged, and a mutation check confirmed the test still goes red when the capture ignores the settled flag. No production code changed.Validation
executor-contract-violation-terminal+ the MCP bundle: 92/92.quotaCache/quotaPreflight: 693/694. The one miss isKimi billing-cycle quota errors remain active…, which has a 100 ms timing tolerance and passes 3/3 on repeat. I could not finish the tip-vs-branch comparison because the .113 disk filled up (ENOSPC).check:open-sse-typecheck0 errors,check:cyclesOK, ESLint 0 errors on changed files.typecheck:coreclean in each fix's worktree.check:file-sizereportsopen-sse/utils/proxyFetch.ts1330 > 1294. That is inherited: this PR does not touch the file, and the growth came from the proxy PRs merged the same day.combo-target-timeout-runner(50 ms vs 20 ms window) failed once on shard 1/4 and passed on the previous head. Timing flake, untouched here.check-docs-counts-sync/ Docs Gates: migration count 183 in the docs vs 186 in code. Inherited, owned by chore(quality): rebaseline the 2026-09-24 merge-batch growth and sync the migration count to 186 #14778 (which currently syncs to 185 and needs one more bump).tests/integration/resilience-http-e2e.test.tswent 0/8 on .113 while the disk was full (server never came up), and I could not re-run it after freeing space. It needs a clean run before this merges.Maintainer rework (merge-batch 2026-09-28 (release drain))
Re-checked every red against the current
release/v3.8.51tip before reconciling:combo-responses-sse-failure-fallback(17/17 on the tip; the conflictingexecuteTargetClassify.ts/validateQuality.ts/ test hunks resolved to the tip's version and thecombo-precontent-stream-retry-detailed-reasonfragment removed) and the zh-TW glossary value (identical on the tip;sr5-glossary-14014fragment removed).chatcore-upstream-timeouts(abort-listener leak),proxyfetch-upstream-status-capture,pack-artifact-policy,flush-empty-retry(4 cases),settings-i18n-keys(sidebar Model catalog keys) andbuild/mcp-bundle-startup.src/lib/db/proxyLogs.ts(andsrc/lib/proxyLogger.ts) importedsanitizeTimingMsfromupstreamStatusCapture.ts, which reachesusage/migrations.ts(top-level await) throughproviderRequestLogging. Every DB module importingproxyLogsbecame async (100 → 227 async inits in the bundle) and the bundle deadlocked again even with thequotaCacheStateleaf.sanitizeTimingMsnow lives in the zero-import leafopen-sse/utils/timingMs.ts;upstreamStatusCapture.tsre-exports it. Proof: tip = red; tip + timing leaf only = red; this branch without the timing leaf = red; both leaves = green.stryker.conf.json: union of the tip's and this PR'stap.testFilesadditions.typecheck:coreclean;check:open-sse-typecheck0 errors;check:cyclesOK;check:file-sizeOK.