fix(routing): publish dashboard events from the round-robin combo loop (#13089) - #13776
Merged
diegosouzapw merged 3 commits intoSep 16, 2026
Merged
diegosouzapw merged 3 commits into
diegosouzapw merged 3 commits into
Conversation
#13089) Round-robin is the only combo strategy that bypasses handleComboChat -> executeTargetAttempt.ts, the path that publishes the combo.target.attempt / combo.target.succeeded / combo.target.failed EventBus events the Combo Studio Live dashboard listens for. roundRobinCombo.ts dispatched directly through handleSingleModel() and never emitted any of those events, so a round-robin combo could complete correctly (200, valid SSE, clean logs) while Live stayed at backlog: 0 forever. Wires the three events at the loop's 6 existing dispatch/outcome points via a new module, rrDashboardEvents.ts (roundRobinCombo.ts is frozen at its file-size cap, so the emitter logic and its call-site construction live there; the frozen-file baseline is bumped by the minimum irreducible footprint of wiring 6 required one-line call sites in). Regression test: tests/unit/issue-13089-roundrobin-live-ws-events.test.ts
…9-combo-live-roundrobin-missing-events
…ing-events (base-red fix #13747)
diegosouzapw
added a commit
that referenced
this pull request
Sep 17, 2026
Third pass on the release/v3.8.51 base-reds. Declaring `vite` in the previous commit was correct but incomplete: check-deps is a human review point against typosquatting, so a newly declared package has to be vouched for by name. Recorded in dependency-allowlist.json with why it is needed — the official Vite build tool, already pinned through overrides, and a required peer of both vitest 5 and @vitejs/plugin-react. That also turns check-deps.test.ts green. check-file-size went red on nine files. One is mine: sse-auth.test.ts grew when the #12080 assertion was rewritten. Three of the four assertions I had added were redundant with the strict deepEqual that follows them, so they are gone and the file grows by 4 lines instead of 8; the cap absorbs the rest. The other eight are production and test files this PR does not touch, grown by other work and never rebaselined — which is the whole reason a base-red drain exists. Each is attributed to the commit that grew it: #12906 (chat.ts, chatHelpers.ts, proxyFetch.ts, stream.ts), #12904 + #12910 (chatCore.ts), and batch_api.test.ts from the same wave. Two of them predate the wave entirely and were already over cap on 3d5baf1 — imageGeneration.ts (#13748) and roundRobinCombo.ts (#13776) — so they were base-reds hiding behind a gate that only surfaced them once the tip was merged in. Both are recorded separately from the wave so the history stays honest about when each cap actually moved. Note for whoever reads the gate next: it counts one line more than `wc -l`, since it measures split length rather than newlines. Finally, #13290 replaced rmSync with cleanupTempDataDir in zcode-executor.test.ts but left the import behind, which the frozen-warning ESLint gate rejects. Removed. Refs #13866
diegosouzapw
added a commit
that referenced
this pull request
Sep 17, 2026
* fix(quality): clear the release/v3.8.51 base-reds 19 failing unit tests plus the API Route Typecheck and mutation-test-coverage gates, all reproduced on the clean tip before touching anything. Ten of the failures share one cause. #13452/#13798 made `*-compatible-*` buildUrl() refuse a connection with no baseUrl instead of quietly defaulting to the real OpenAI/Anthropic API — which would ship the operator's stored key to a public third party. The guard is right; three fixtures still built those connections unhydrated, and one of them put baseUrl at the top level of credentials, where the chat path never reads it. The rest: - modelDiscovery.ts missed the VertexModelMetadataProvenance cast that its read-path twin in db/models/synced.ts already had — both written by #12471. - A provider-test regexp carried raw 0x00/0x1f bytes, which makes git, GitHub and ripgrep treat the file as binary. Same character class, written with escapes instead of the bytes themselves. - #13399 (Agnes AI China) adds "agnes-cn" + "agnescn": the only two provider prefixes since the count was last set (412 -> 414). Everything else added in that range is model ids. - The free-tier budget card SVG was stale (443 -> 452 models); regenerated by its own script. - Three new tests were missing from stryker.conf.json tap.testFiles, so the mutants they kill did not count. Three guards asserted syntax rather than the invariant they protect, and broke when the source legitimately changed. Each was re-expressed and then verified by mutating the source back: - #2331 required modelEffort to head the rawEffort chain; #13556 deliberately put the server-selected force rule first. The real invariant is relative — modelEffort outranks the defaults a client injects — and it still trips when explicitReasoning is moved ahead of it. - The OAuth loopback guard matched the isLocalhost arm literally; #9944 added `&& !opts?.manualLoopback`. It now matches the arm whatever guards it, and still fails when the hint stops being built. - The i18n scanner flagged dynamically-built keys — t("effort." + mode) reaches it as a literal prefix, never a string. It now accepts a prefix that resolves to a namespace holding messages, and still fails when the namespace is gone. tests/unit/sse-auth.test.ts (#12080) expected a bare null where #13879 now returns the key-policy diagnostic — the same sentinel shape the terminal-state path has used since #12441. The assertion was rewritten to the constraint #12080 actually protects: nothing usable comes back and neither connection leaks. The contract risk that remains — those sentinels are truthy, and executeWebSearch treats any truthy value as a credential — is filed as #13945 rather than widened into this PR. Refs #13866 * fix(quality): clear the second wave of release/v3.8.51 base-reds The tip moved 13 commits while the first pass was running and brought its own reds. All reproduced locally on the merged tree first. vitest 4.1.11 -> 5.0.0 in the #13661 development-group bump is a major, and vitest 5 moved `vite` from a dependency to a peerDependency. This repo only ever declared `vite` under `overrides`, which pins a version but installs nothing, so `npm ci` stopped providing it and the Vitest job died at startup with ERR_MODULE_NOT_FOUND. Declared as the devDependency it actually is — the same ^8.0.16 the override already pinned, and what @vitejs/plugin-react asks for as a peer — and regenerated the lockfile: 684 lines added, none changed. #12909 filtered a mapped array with `toolCall is JsonRecord`, but the element type is the tool-call literal or null, and a predicate's type has to be assignable to the parameter's (TS2677). Narrowed by the element's own type instead; the literal still satisfies JsonRecord at the return. #12906 added `|| result.errorCode === "empty_response"` to the stream-failure condition and Prettier rewrapped it, so the #8928 probe — which located the branch by an exact four-line string — stopped finding it. It now matches on what the branch tests rather than how it is typeset, and still fails when the eviction call is removed. probe-7293 is the visible half of a real conflict, filed as #13948. #7293 merges a mid-array system into index 0; #12908, landed later, demotes it to "user" in place instead. Both target the same constraint and only one can win, and the combination also reorders: the pre-translation hoist moves the turn forward expecting it to stay a system message, then the demotion converts it where it now sits, ahead of the conversation. Choosing between the two strategies is a product call, not a base-red one, so the test was realigned to assert the half that protects the caller — the instruction survives, as a user turn — and pins the current ordering with a pointer to the issue, so the eventual decision shows up as a deliberate test change instead of a silent regression. Refs #13866, #13948 * fix(quality): allowlist vite, rebaseline tip growth, drop a dead import Third pass on the release/v3.8.51 base-reds. Declaring `vite` in the previous commit was correct but incomplete: check-deps is a human review point against typosquatting, so a newly declared package has to be vouched for by name. Recorded in dependency-allowlist.json with why it is needed — the official Vite build tool, already pinned through overrides, and a required peer of both vitest 5 and @vitejs/plugin-react. That also turns check-deps.test.ts green. check-file-size went red on nine files. One is mine: sse-auth.test.ts grew when the #12080 assertion was rewritten. Three of the four assertions I had added were redundant with the strict deepEqual that follows them, so they are gone and the file grows by 4 lines instead of 8; the cap absorbs the rest. The other eight are production and test files this PR does not touch, grown by other work and never rebaselined — which is the whole reason a base-red drain exists. Each is attributed to the commit that grew it: #12906 (chat.ts, chatHelpers.ts, proxyFetch.ts, stream.ts), #12904 + #12910 (chatCore.ts), and batch_api.test.ts from the same wave. Two of them predate the wave entirely and were already over cap on 3d5baf1 — imageGeneration.ts (#13748) and roundRobinCombo.ts (#13776) — so they were base-reds hiding behind a gate that only surfaced them once the tip was merged in. Both are recorded separately from the wave so the history stays honest about when each cap actually moved. Note for whoever reads the gate next: it counts one line more than `wc -l`, since it measures split length rather than newlines. Finally, #13290 replaced rmSync with cleanupTempDataDir in zcode-executor.test.ts but left the import behind, which the frozen-warning ESLint gate rejects. Removed. Refs #13866
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
diegosouzapw#13089) (diegosouzapw#13776) Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
* fix(quality): clear the release/v3.8.51 base-reds 19 failing unit tests plus the API Route Typecheck and mutation-test-coverage gates, all reproduced on the clean tip before touching anything. Ten of the failures share one cause. diegosouzapw#13452/diegosouzapw#13798 made `*-compatible-*` buildUrl() refuse a connection with no baseUrl instead of quietly defaulting to the real OpenAI/Anthropic API — which would ship the operator's stored key to a public third party. The guard is right; three fixtures still built those connections unhydrated, and one of them put baseUrl at the top level of credentials, where the chat path never reads it. The rest: - modelDiscovery.ts missed the VertexModelMetadataProvenance cast that its read-path twin in db/models/synced.ts already had — both written by diegosouzapw#12471. - A provider-test regexp carried raw 0x00/0x1f bytes, which makes git, GitHub and ripgrep treat the file as binary. Same character class, written with escapes instead of the bytes themselves. - diegosouzapw#13399 (Agnes AI China) adds "agnes-cn" + "agnescn": the only two provider prefixes since the count was last set (412 -> 414). Everything else added in that range is model ids. - The free-tier budget card SVG was stale (443 -> 452 models); regenerated by its own script. - Three new tests were missing from stryker.conf.json tap.testFiles, so the mutants they kill did not count. Three guards asserted syntax rather than the invariant they protect, and broke when the source legitimately changed. Each was re-expressed and then verified by mutating the source back: - diegosouzapw#2331 required modelEffort to head the rawEffort chain; diegosouzapw#13556 deliberately put the server-selected force rule first. The real invariant is relative — modelEffort outranks the defaults a client injects — and it still trips when explicitReasoning is moved ahead of it. - The OAuth loopback guard matched the isLocalhost arm literally; diegosouzapw#9944 added `&& !opts?.manualLoopback`. It now matches the arm whatever guards it, and still fails when the hint stops being built. - The i18n scanner flagged dynamically-built keys — t("effort." + mode) reaches it as a literal prefix, never a string. It now accepts a prefix that resolves to a namespace holding messages, and still fails when the namespace is gone. tests/unit/sse-auth.test.ts (diegosouzapw#12080) expected a bare null where diegosouzapw#13879 now returns the key-policy diagnostic — the same sentinel shape the terminal-state path has used since diegosouzapw#12441. The assertion was rewritten to the constraint diegosouzapw#12080 actually protects: nothing usable comes back and neither connection leaks. The contract risk that remains — those sentinels are truthy, and executeWebSearch treats any truthy value as a credential — is filed as diegosouzapw#13945 rather than widened into this PR. Refs diegosouzapw#13866 * fix(quality): clear the second wave of release/v3.8.51 base-reds The tip moved 13 commits while the first pass was running and brought its own reds. All reproduced locally on the merged tree first. vitest 4.1.11 -> 5.0.0 in the diegosouzapw#13661 development-group bump is a major, and vitest 5 moved `vite` from a dependency to a peerDependency. This repo only ever declared `vite` under `overrides`, which pins a version but installs nothing, so `npm ci` stopped providing it and the Vitest job died at startup with ERR_MODULE_NOT_FOUND. Declared as the devDependency it actually is — the same ^8.0.16 the override already pinned, and what @vitejs/plugin-react asks for as a peer — and regenerated the lockfile: 684 lines added, none changed. diegosouzapw#12909 filtered a mapped array with `toolCall is JsonRecord`, but the element type is the tool-call literal or null, and a predicate's type has to be assignable to the parameter's (TS2677). Narrowed by the element's own type instead; the literal still satisfies JsonRecord at the return. diegosouzapw#12906 added `|| result.errorCode === "empty_response"` to the stream-failure condition and Prettier rewrapped it, so the diegosouzapw#8928 probe — which located the branch by an exact four-line string — stopped finding it. It now matches on what the branch tests rather than how it is typeset, and still fails when the eviction call is removed. probe-7293 is the visible half of a real conflict, filed as diegosouzapw#13948. diegosouzapw#7293 merges a mid-array system into index 0; diegosouzapw#12908, landed later, demotes it to "user" in place instead. Both target the same constraint and only one can win, and the combination also reorders: the pre-translation hoist moves the turn forward expecting it to stay a system message, then the demotion converts it where it now sits, ahead of the conversation. Choosing between the two strategies is a product call, not a base-red one, so the test was realigned to assert the half that protects the caller — the instruction survives, as a user turn — and pins the current ordering with a pointer to the issue, so the eventual decision shows up as a deliberate test change instead of a silent regression. Refs diegosouzapw#13866, diegosouzapw#13948 * fix(quality): allowlist vite, rebaseline tip growth, drop a dead import Third pass on the release/v3.8.51 base-reds. Declaring `vite` in the previous commit was correct but incomplete: check-deps is a human review point against typosquatting, so a newly declared package has to be vouched for by name. Recorded in dependency-allowlist.json with why it is needed — the official Vite build tool, already pinned through overrides, and a required peer of both vitest 5 and @vitejs/plugin-react. That also turns check-deps.test.ts green. check-file-size went red on nine files. One is mine: sse-auth.test.ts grew when the diegosouzapw#12080 assertion was rewritten. Three of the four assertions I had added were redundant with the strict deepEqual that follows them, so they are gone and the file grows by 4 lines instead of 8; the cap absorbs the rest. The other eight are production and test files this PR does not touch, grown by other work and never rebaselined — which is the whole reason a base-red drain exists. Each is attributed to the commit that grew it: diegosouzapw#12906 (chat.ts, chatHelpers.ts, proxyFetch.ts, stream.ts), diegosouzapw#12904 + diegosouzapw#12910 (chatCore.ts), and batch_api.test.ts from the same wave. Two of them predate the wave entirely and were already over cap on 8a95ffa — imageGeneration.ts (diegosouzapw#13748) and roundRobinCombo.ts (diegosouzapw#13776) — so they were base-reds hiding behind a gate that only surfaced them once the tip was merged in. Both are recorded separately from the wave so the history stays honest about when each cap actually moved. Note for whoever reads the gate next: it counts one line more than `wc -l`, since it measures split length rather than newlines. Finally, diegosouzapw#13290 replaced rmSync with cleanupTempDataDir in zcode-executor.test.ts but left the import behind, which the frozen-warning ESLint gate rejects. Removed. Refs diegosouzapw#13866
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.
Closes #13089
Root cause (short)
open-sse/services/combo/roundRobinCombo.tsis the one combo strategy that bypasseshandleComboChat→executeTargetAttempt.ts, the path that publishes thecombo.target.attempt/combo.target.succeeded/combo.target.failedEventBus events the Combo Studio Live dashboard's "combo" channel listens for. Round-robin dispatches directly throughhandleSingleModel()and never emitted any of those events, so a round-robin combo (e.g. a round-robin combo with many models) could complete perfectly — 200, correct SSE stream, clean logs — while the Live dashboard stayed atbacklog: 0forever. Every other strategy (priority, weighted, auto, etc.) goes through the path that does emit these events, so they were unaffected.Fix
Wire the three EventBus events into round-robin's 6 existing dispatch/outcome points (attempt-before-dispatch, quality-check-failed, success, 499 client-disconnect, local-queue-capacity, and the terminal per-target failure path).
roundRobinCombo.tsis frozen at its file-size cap (config/quality/file-size-baseline.json, precedent from #12884), so the emitter logic lives in a new module,open-sse/services/combo/rrDashboardEvents.ts— a small factory bound to one target's identity (comboName,targetIndex,provider,model) so each call site in the frozen file is a single line (rrEvents.attempt(),rrEvents.succeeded(latencyMs),rrEvents.failed(error, latencyMs)). The frozen baseline forroundRobinCombo.tsis bumped from 1205 to 1213 lines with a justification entry — the minimum irreducible footprint for wiring 6 required call sites into 6 fixed control-flow points, matching the same file's own prior rebaseline precedent for #12884.Monitoring-only change — no impact on routing, dispatch, or response correctness.
Regression test (path + RED output excerpt on unfixed code + GREEN excerpt)
tests/unit/issue-13089-roundrobin-live-ws-events.test.ts— two cases: a successful round-robin completion must publishcombo.target.attempt+combo.target.succeeded, and an exhausted round-robin target must publishcombo.target.failed.RED (unfixed code):
GREEN (fixed code):
Run command:
DATA_DIR=$(mktemp -d) timeout 300 node --import tsx/esm --test --test-force-exit tests/unit/issue-13089-roundrobin-live-ws-events.test.tsGates run
npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>→ exit 0, no new warnings.npm run check:open-sse-typecheck→openSseTypecheckErrors=0.node scripts/check/check-file-size.mjs→roundRobinCombo.tsfixed via baseline bump (justified above);open-sse/utils/stream.ts(3115 > 3098) is pre-existing base drift, untouched by this branch (emptygit diffagainstorigin/release/v3.8.51).node scripts/check/check-complexity.mjs→ OK, 2824 violations (baseline 3218).node scripts/check/check-cognitive-complexity.mjs→ OK, 1276 violations (baseline 1437).node scripts/check/check-test-discovery.mjs→ OK, new test file discovered.Existing tests aligned
None weakened or changed. Ran the full round-robin regression surface to confirm no behavior change:
tests/unit/combo/round-robin-combo.test.ts,tests/unit/combo-config.test.ts,tests/unit/combo/combo-attempt-loop.test.ts,tests/unit/combo/combo-exhausted-skip.test.ts(61 tests),tests/unit/issue-11912-opencode-roundrobin-collapse.test.ts,tests/unit/combo-rr-session-stickiness-3825.test.ts(4 tests) — all green.