Skip to content

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
diegosouzapw merged 15 commits into
release/v3.8.51from
fix/release-v3.8.51-basereds-r5
Sep 29, 2026

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

⚠️ base-red inherited: #14547

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:

Red Bisected to Kind
chatcore-upstream-timeouts: abort listener left on the client signal #14342 product regression, the #12406 leak is back
combo-responses-sse-failure-fallback (4 cases) #14314 product regression (2 cases) + intentional message change (2 cases)
i18n-glossary-consistency-check (2 cases) #14014 zh-TW value off the glossary
proxyfetch-upstream-status-capture #14311 stale test timing, product intentional
build/mcp-bundle-startup #14555 product regression: the MCP bundle no longer loads
pack-artifact-policy #14712 test not updated with the new required path
settings-i18n-keys (sidebar) #14684 new sidebar item shipped without its en.json keys
flush-empty-retry (4 cases) #14691 stale test: bounded read now stops at the first useful chunk

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 abort listener 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.ts now owns the base reason and exports isStreamingUpstreamErrorReason(), which accepts the bare reason or base: detail and nothing else. executeTargetClassify.ts uses 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.ts import @/domain/quotaCache. quotaCache → usage → usage/openrouter → openrouterQuotaFetcher → quotaPreflight closes an ESM init cycle. In the esbuild MCP bundle every module initializes asynchronously, so the cycle deadlocks and import(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.ts re-exports them, so no caller changes, and quotaPreflight imports 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.mjs a required tarball path on purpose (it runs on every boot), but pack-artifact-policy.test.ts still listed the old missing-paths set. The test now expects it (20/20).

Sidebar Model catalog keys (#14684). #14684 added a model-catalog sidebar entry with i18nKey: modelCatalog / subtitleKey: modelCatalogSubtitle but only inline fallbacks. The two keys now exist in en.json (same text as the fallbacks), plus pt-BR and vi, 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 (verdict pass) for a turn that already carries content; only content-free turns are drained. Four cases still fed a content chunk and expected drained text or idle (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 expects early-pass with the same pass verdict, and the outcome test also pins early-pass for 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:1 fail 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. The 429 assertion is unchanged, and a mutation check confirmed the test still goes red when the capture ignores the settled flag. No production code changed.

Validation

Maintainer rework (merge-batch 2026-09-28 (release drain))

Re-checked every red against the current release/v3.8.51 tip before reconciling:

  • Already fixed on the tip, dropped from this PR: combo-responses-sse-failure-fallback (17/17 on the tip; the conflicting executeTargetClassify.ts / validateQuality.ts / test hunks resolved to the tip's version and the combo-precontent-stream-retry-detailed-reason fragment removed) and the zh-TW glossary value (identical on the tip; sr5-glossary-14014 fragment removed).
  • Still red on the tip, kept: 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) and build/mcp-bundle-startup.
  • New MCP bundle deadlock from feat(proxy-logs): record per-attempt upstream timing on proxy log rows #14892 (merged today): src/lib/db/proxyLogs.ts (and src/lib/proxyLogger.ts) imported sanitizeTimingMs from upstreamStatusCapture.ts, which reaches usage/migrations.ts (top-level await) through providerRequestLogging. Every DB module importing proxyLogs became async (100 → 227 async inits in the bundle) and the bundle deadlocked again even with the quotaCacheState leaf. sanitizeTimingMs now lives in the zero-import leaf open-sse/utils/timingMs.ts; upstreamStatusCapture.ts re-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's tap.testFiles additions.
  • Validation on the merged tree: the kept suites + quota-cache / quota-preflight / proxy-log suites + MCP bundle 162/162; typecheck:core clean; check:open-sse-typecheck 0 errors; check:cycles OK; check:file-size OK.

…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
#14712 (GHSA-2pg2-xm9r-8544) added bin/cli/privateDataDir.mjs to
PACK_ARTIFACT_REQUIRED_PATHS on purpose (it runs on every boot), but the
missing-paths assertion in pack-artifact-policy.test.ts still listed the old
set, so the test failed on the release tip.

Refs #14547
@diegosouzapw diegosouzapw changed the title fix: clear five base-reds from the 2026-09-24 merge wave — abort-listener leak, pre-content retry, MCP bundle deadlock, zh-TW glossary, proxy-status test (#14547) fix: clear six base-reds from the 2026-09-24 merge wave — abort-listener leak, pre-content retry, MCP bundle deadlock, zh-TW glossary, proxy-status and pack-policy tests (#14547) Sep 25, 2026
#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
@diegosouzapw diegosouzapw changed the title fix: clear six base-reds from the 2026-09-24 merge wave — abort-listener leak, pre-content retry, MCP bundle deadlock, zh-TW glossary, proxy-status and pack-policy tests (#14547) fix: clear seven base-reds from the 2026-09-24 merge wave — abort-listener leak, pre-content retry, MCP bundle deadlock, zh-TW glossary, sidebar keys, proxy-status and pack-policy tests (#14547) Sep 25, 2026
pt-BR and vi carry key-parity tests against en.json, so the two English
sidebar keys added for #14684 have to exist there too (i18n-pt-br and
i18n-vi-completeness failed on the previous head). Values follow each locale's
existing sidebar terms (Provedores / Nhà cung cấp).

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
@diegosouzapw diegosouzapw changed the title fix: clear seven base-reds from the 2026-09-24 merge wave — abort-listener leak, pre-content retry, MCP bundle deadlock, zh-TW glossary, sidebar keys, proxy-status and pack-policy tests (#14547) 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) Sep 25, 2026
…-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
diegosouzapw merged commit b3fbe34 into release/v3.8.51 Sep 29, 2026
11 checks passed
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!
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.

1 participant