Skip to content

fix(sse): shallow per-target copy for the combo attempt body (#7847) - #8553

Merged
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.49from
MumuTW:fix/7847-combo-attempt-body-cow
Jul 26, 2026
Merged

diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.49from
MumuTW:fix/7847-combo-attempt-body-cow

Conversation

@MumuTW

@MumuTW MumuTW commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

The largest of the #7847 reductions, plus a real cross-target leak found on the way. Pairs with #8549 (benchmark) and #8550 (entry clone).

Numbers

combo.ts deep-cloned the request body for every target. On a 3.05 MiB agent request:

targets before after
3 9.53 MiB (3.12x wire) ~0.001 MiB
5 15.89 MiB (5.19x wire) ~0.000 MiB
10 31.78 MiB (10.39x wire) ~0.001 MiB

The deep clone scaled linearly with the target count; the shallow copy is constant and effectively free.

Why a shallow copy is sufficient

The isolation the clone bought only ever needed to contain top-level scalar writes. The complete mutation surface on this path is two assignments:

combo.ts     bodyRecord.max_tokens = ...   (reasoning buffer)
chatCore.ts  body.model = model            (Background Task Redirection T41)

Nothing mutates the nested payload. applyCompression and injectUniversalHandoffBody both return new objects — verified empirically, not by reading: probed with a before/after deep-compare and a frozen input, neither touches its argument. So a fresh top-level object per target gives identical isolation while sharing the expensive messages/tools arrays.

The leak this found — round-robin was already broken

handleRoundRobinCombo already used a shallow copy, but took it only when the reasoning buffer actually changed max_tokens:

let attemptBody = body;                       // shared with the caller
if (bufferedMaxTokens !== currentMaxTokens) { // ...only sometimes copied
  attemptBody = { ...bodyRecord, max_tokens: bufferedMaxTokens };
}

Every other attempt shared the callers object outright, so chatCores body.model = model on one target rewrote the model for the next. The new test reproduces it on the unmodified code:

not ok 3 - round-robin: a target's in-place write must not leak into the next target
  target 1 received model "mutated-by-openai/gpt-4o-mini" — a previous target's write leaked through

priority and fill-first passed, because the deep clone was covering for them. In production this is a Background Task Redirection firing on one round-robin target and the next target's upstream request carrying the degraded model name. The copy is now unconditional.

I raised this as an unconfirmed risk during assessment; the test settled it. Fixing it here rather than separately because it is the same defect (per-target copy discipline), the same file, and the same test file covers all strategies.

Tests pin the invariant, not the implementation

Deliberately written so the clone strategy can change again without anyone re-deriving mutation sites by grep:

  1. a target's in-place write must not leak into the next — priority / fill-first / round-robin; the stub reproduces chatCore's body.model write, which is the real downstream mutation this exists to contain
  2. the caller's body is never mutated — handleComboChat treats body as read-only
  3. the per-target copy stays shallow — all targets point at one messages array, and the message objects are still the caller's
  4. freeze probe — a deep-frozen body through the combo loop; any in-place write throws. Scope note: handleSingleModel is stubbed, so this covers combo's own body handling, not chatCore or the executors — test 1 is what covers a mutating downstream.

Verification

suite result
new isolation tests 6/6 pass (test 3 was red on unmodified code)
combo regression sweep (450 files) pass, except one base-red below
typecheck:core, check:cycles, check:any-budget:t11, check-file-size clean

Pre-existing failures confirmed red on upstream/release/v3.8.49 with combo.ts untouched, so not from this PR: tests/unit/autoCombo/provider-family-combos, tests/unit/autoCombo/tieredRotation (vitest-authored, they fail under node:test), and live repo: no NEW unexported db modules beyond the frozen allowlist.

Note the check-file-size gate is shrink-only and combo.ts is frozen at 3642 lines — the explanatory comments had to be trimmed to fit. That is why they are terser than the reasoning warrants; the test file carries the full rationale.

Inherited base-red (not from this PR)

release/v3.8.49 is red on lint (stale suppression, fixed by #8544) and file-size (providers/page.tsx, tokenHealthCheck.ts — fixed by #8532 / #8524).

@diegosouzapw

Copy link
Copy Markdown
Owner

Reviewed #8553 by checking out the PR branch in an isolated worktree against origin/release/v3.8.49 and running the new test suite both ways.

On unmodified combo.ts (test file added, production code untouched), 2 of the 6 new tests fail as expected: the round-robin cross-target leak reproduces exactly as described (target N+1 receives model "mutated-by-" instead of the original), and the shallow-copy-shape test correctly fails since the base still deep-clones. With your fix applied, all 6 pass. typecheck:core, check:any-budget:t11, and check:file-size (combo.ts is at 3641/3642 lines, under the frozen cap) are all clean on the PR branch. The file-size violations I do see (providers/page.tsx, tokenHealthCheck.ts) and the check:db-rules reds are all in unrelated files — confirmed pre-existing on the base tip, not introduced here.

One item to close before merge: your new test file exercises circuitBreaker.ts (it resets the breaker in beforeEach), and check:mutation-test-coverage --strict is strict about that mapping. I re-measured on the pristine tip: it reports 3 drift entries there (accountFallback.ts, error.ts, comboPredicates.ts) and 4 on your branch — so the circuitBreaker.ts entry is new drift from this PR. Since #8538 is about to bring that gate to zero, could you add tests/unit/combo-attempt-body-isolation-7847.test.ts to stryker.conf.json's tap.testFiles so this doesn't immediately re-red it?

One heads-up on merge ordering: #8555 and #8558 each independently reformat the exact same computeCompatRejectedTargets(...) call (byte-identical wrap) that you also reformat here, so whichever of the three merges last will hit a small mechanical conflict at that spot — trivial to resolve, no semantic risk since it's pure formatting.

Good catch finding the round-robin leak while working the shallow-copy change — that's a real production bug (Background Task Redirection bleeding into the next target's model) that the deep clone was masking.

MumuTW added 2 commits July 25, 2026 22:33
…uzapw#7847)

combo.ts deep-cloned the request body for every target. On a 3.05 MiB agent request that
is 9.53 MiB at 3 targets, and it scales linearly:

  3 targets    9.53 MiB (3.12x wire)  ->  ~0.001 MiB
  5 targets   15.89 MiB (5.19x wire)  ->  ~0.000 MiB
 10 targets   31.78 MiB (10.39x wire) ->  ~0.001 MiB

The isolation it bought only ever needed to contain TOP-LEVEL SCALAR writes. The full
mutation surface on this path is two assignments:

  combo.ts     bodyRecord.max_tokens = ...   (reasoning buffer)
  chatCore.ts  body.model = model            (Background Task Redirection T41)

Nothing mutates the nested payload; applyCompression and injectUniversalHandoffBody both
return new objects (verified empirically -- neither touches its input, and both tolerate a
frozen one). So a fresh top-level object per target gives identical isolation while sharing
the expensive messages/tools arrays.

Also fixes a REAL cross-target leak in handleRoundRobinCombo. It already used a shallow
copy, but took it only when the reasoning buffer actually changed max_tokens -- every other
attempt shared the caller's object outright. The new test reproduces it on the unmodified
code: target 2 received model "mutated-by-openai/gpt-4o-mini". In production that is a
Background Task Redirection on one round-robin target rewriting body.model for the next.
The copy is now unconditional.

The invariant is pinned by tests rather than by a comment listing mutation sites, so the
clone strategy can change again without anyone re-deriving them by hand:
 - a target's in-place write must not leak into the next (priority / fill-first /
   round-robin; the stub reproduces chatCore's body.model write)
 - the caller's body is never mutated
 - the per-target copy stays shallow (targets share one messages array)
 - freeze probe: combo's own body handling performs no in-place writes
…tap.testFiles

The new test resets the circuit breaker in beforeEach, so it counts as a covering test
for src/shared/utils/circuitBreaker.ts. Without registering it,
check:mutation-test-coverage --strict reported a 4th drift entry that was not there on the
pristine tip -- new drift introduced by this PR. Registered in sorted position; the gate is
back to the 3 pre-existing entries (accountFallback.ts, error.ts, comboPredicates.ts) that
diegosouzapw#8538 addresses.
@MumuTW

MumuTW commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Both items are handled.

stryker: tests/unit/combo-attempt-body-isolation-7847.test.ts is registered in stryker.conf.json's tap.testFiles (commit e480c7089). The rebase merged cleanly with #8538's entries, so all three are present — #8376, #8396 and this one — and the gate shouldn't re-red once #8538 brings it to zero.

Merge ordering: the computeCompatRejectedTargets conflict is gone. #8555 is closed in favour of #8548 (agreed with your read there), and dropping #8555's commit out of #8558 removed that reformat from that branch too — #8558 is now a single 2-line hunk at ~1819. I test-merged all three pairwise against the current tip with git merge-tree --write-tree: #8548 × #8553, #8548 × #8558, #8553 × #8558 all clean, in any order.

Also rebased onto 4053e2314, so the base-red gates are fixed upstream rather than carved out (#8544, #8534, #8539, #8561). Re-verified on the rebased head: 6/6 pass, check:file-size OK.

@MumuTW

MumuTW commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Correction to my comment above: Fast Quality Gates will still be red after the rebase, and it isn't this PR. #8561 fixed check:file-size and thereby unmasked a fifth base-red gate behind it in the same job — check:complexity-ratchets (complexity 2169 > baseline 2130, cognitiveComplexity 956 > 951).

I measured it on pristine detached checkouts with an empty working tree: 4053e2314 (current tip) and 30709255c (the base you reviewed against) both report 2169 / 956 — identical to this PR's head, so it predates #8561 and is not caused by anything here. It was hidden because check:file-size ran earlier in the same bash -e job and short-circuited it. Full measurement table in my comment on #8546.

The gate is in quality.yml's fast-gates job, so it's red for every PR against release/v3.8.49 regardless of content. Everything else on this PR is green and verified locally.

@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @MumuTW — merged into release/v3.8.49 via the local merge-train (validated as one combined tree: full test:unit + test:vitest 274/274 on the 32-core box, tip d4b9ce6016). Your commit keeps its authorship. 🚀

HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…uzapw#7847) (diegosouzapw#8553)

* fix(sse): shallow per-target copy for the combo attempt body (diegosouzapw#7847)

combo.ts deep-cloned the request body for every target. On a 3.05 MiB agent request that
is 9.53 MiB at 3 targets, and it scales linearly:

  3 targets    9.53 MiB (3.12x wire)  ->  ~0.001 MiB
  5 targets   15.89 MiB (5.19x wire)  ->  ~0.000 MiB
 10 targets   31.78 MiB (10.39x wire) ->  ~0.001 MiB

The isolation it bought only ever needed to contain TOP-LEVEL SCALAR writes. The full
mutation surface on this path is two assignments:

  combo.ts     bodyRecord.max_tokens = ...   (reasoning buffer)
  chatCore.ts  body.model = model            (Background Task Redirection T41)

Nothing mutates the nested payload; applyCompression and injectUniversalHandoffBody both
return new objects (verified empirically -- neither touches its input, and both tolerate a
frozen one). So a fresh top-level object per target gives identical isolation while sharing
the expensive messages/tools arrays.

Also fixes a REAL cross-target leak in handleRoundRobinCombo. It already used a shallow
copy, but took it only when the reasoning buffer actually changed max_tokens -- every other
attempt shared the caller's object outright. The new test reproduces it on the unmodified
code: target 2 received model "mutated-by-openai/gpt-4o-mini". In production that is a
Background Task Redirection on one round-robin target rewriting body.model for the next.
The copy is now unconditional.

The invariant is pinned by tests rather than by a comment listing mutation sites, so the
clone strategy can change again without anyone re-deriving them by hand:
 - a target's in-place write must not leak into the next (priority / fill-first /
   round-robin; the stub reproduces chatCore's body.model write)
 - the caller's body is never mutated
 - the per-target copy stays shallow (targets share one messages array)
 - freeze probe: combo's own body handling performs no in-place writes

* test(sse): register the combo attempt-body isolation test in stryker tap.testFiles

The new test resets the circuit breaker in beforeEach, so it counts as a covering test
for src/shared/utils/circuitBreaker.ts. Without registering it,
check:mutation-test-coverage --strict reported a 4th drift entry that was not there on the
pristine tip -- new drift introduced by this PR. Registered in sorted position; the gate is
back to the 3 pre-existing entries (accountFallback.ts, error.ts, comboPredicates.ts) that
diegosouzapw#8538 addresses.
@MumuTW
MumuTW deleted the fix/7847-combo-attempt-body-cow branch September 5, 2026 10:19
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…uzapw#7847) (diegosouzapw#8553)

* fix(sse): shallow per-target copy for the combo attempt body (diegosouzapw#7847)

combo.ts deep-cloned the request body for every target. On a 3.05 MiB agent request that
is 9.53 MiB at 3 targets, and it scales linearly:

  3 targets    9.53 MiB (3.12x wire)  ->  ~0.001 MiB
  5 targets   15.89 MiB (5.19x wire)  ->  ~0.000 MiB
 10 targets   31.78 MiB (10.39x wire) ->  ~0.001 MiB

The isolation it bought only ever needed to contain TOP-LEVEL SCALAR writes. The full
mutation surface on this path is two assignments:

  combo.ts     bodyRecord.max_tokens = ...   (reasoning buffer)
  chatCore.ts  body.model = model            (Background Task Redirection T41)

Nothing mutates the nested payload; applyCompression and injectUniversalHandoffBody both
return new objects (verified empirically -- neither touches its input, and both tolerate a
frozen one). So a fresh top-level object per target gives identical isolation while sharing
the expensive messages/tools arrays.

Also fixes a REAL cross-target leak in handleRoundRobinCombo. It already used a shallow
copy, but took it only when the reasoning buffer actually changed max_tokens -- every other
attempt shared the caller's object outright. The new test reproduces it on the unmodified
code: target 2 received model "mutated-by-openai/gpt-4o-mini". In production that is a
Background Task Redirection on one round-robin target rewriting body.model for the next.
The copy is now unconditional.

The invariant is pinned by tests rather than by a comment listing mutation sites, so the
clone strategy can change again without anyone re-deriving them by hand:
 - a target's in-place write must not leak into the next (priority / fill-first /
   round-robin; the stub reproduces chatCore's body.model write)
 - the caller's body is never mutated
 - the per-target copy stays shallow (targets share one messages array)
 - freeze probe: combo's own body handling performs no in-place writes

* test(sse): register the combo attempt-body isolation test in stryker tap.testFiles

The new test resets the circuit breaker in beforeEach, so it counts as a covering test
for src/shared/utils/circuitBreaker.ts. Without registering it,
check:mutation-test-coverage --strict reported a 4th drift entry that was not there on the
pristine tip -- new drift introduced by this PR. Registered in sorted position; the gate is
back to the 3 pre-existing entries (accountFallback.ts, error.ts, comboPredicates.ts) that
diegosouzapw#8538 addresses.
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.

2 participants