Skip to content

feat(proxy): coalesce the duplicate session-start sidecar send (opt-in) - #337

Open
Gunther-Schulz wants to merge 2 commits into
cnighswonger:mainfrom
Gunther-Schulz:pr/coalesce-sidecar
Open

feat(proxy): coalesce the duplicate session-start sidecar send (opt-in)#337
Gunther-Schulz wants to merge 2 commits into
cnighswonger:mainfrom
Gunther-Schulz:pr/coalesce-sidecar

Conversation

@Gunther-Schulz

Copy link
Copy Markdown
Contributor

Claude Code sends the same session-start sidecar request twice, ~6–25 ms apart, byte-identical. Both are answered, and both are charged. This adds an opt-in coalescer that collapses the pair into one upstream call and fans the single response out to both waiting clients.

Off by default. It does nothing unless CACHE_FIX_COALESCE_SIDECAR=1 is set.

The measurement

Not an inference from the code — this is what one day's traffic on one machine looks like, joined against the completion records by request id:

144 duplicate pairs / 114 streaks (longest streak: 11 sends)
258 requests involved
134 of those requests billed
 55 streaks billed more than once

The session-start class specifically: haiku, nMsg=1, max_tokens=32000, arriving at capture lines 3–5, 6–25 ms apart. 47 instances in one measurement window.

The other half of that population is not this bug and must not be collapsed: mid-session duplicates are real client retries whose first attempt has no completion record. Suppressing those would leave a genuine request unanswered. That distinction is the whole design constraint here.

What it does

Four conditions must all hold before anything is coalesced, and they exist to keep the retry class out:

  • the request matches the session-start sidecar shape;
  • a leader for the same coalesce key is in flight;
  • it arrived inside a 50 ms window (COALESCE_WINDOW_MS);
  • the bodies are byte-identical.

The follower then waits on the leader's response and receives the same bytes. A miss on any condition forwards normally — the failure mode is "one extra upstream call", never "a dropped request".

Tests

test/duplicate-coalesce.test.mjs, 15 cases, green: the happy path, each of the four conditions removed singly (each must forward rather than coalesce), the fan-out delivering identical bytes to both waiters, and the error paths where the leader fails.

npm test on this branch: 1796 tests, 1795 pass, 0 fail (one skip, pre-existing).

Scope, and what deliberately is not here

Two files plus one helper. The second commit carries tools/tmpdir.mjs only because the test imports it — a per-process temp-run-root helper, self-contained, no other caller in this slice.

Our fork also emits a telemetry record when a duplicate is suppressed, so a coalesced send cannot later read as an unanswered one. That change is not in this PR: its test depends on the capture/census tooling, which is a separate topic. Ported here it would have widened this PR into that one, so it rides with that work instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RMiYvNxKq6G9gfJMzArm4q

Gunther-Schulz and others added 2 commits August 14, 2026 19:59
…ated off

CC issues one sidecar request twice, 6-25 ms apart on 47/47 measured pairs,
with distinct upstream request-ids and two completed usage-log records --
both answered, both charged. Dropping the second is unavailable, since two
client requests are in flight and each is owed a response, so the only safe
shape is one upstream call serving both callers.

Four conditions, all required, and the mid-session duplicate class -- where
the second send is a legitimate retry -- fails on nMsg alone, which is the
discriminator the row asked for.

Fan-out sits at the RESPONSE WRITER, not at the upstream reader: the
extension pass and the telemetry record run once, so both callers receive
byte-identical post-pipeline output. Tee-ing the raw upstream would hand the
follower unmutated bytes while the leader got the pipeline's, and fidelity
outranks cache here.

TWO ARMS WERE NOT DISCRIMINATING AND THE MUTATION PROOF IS WHAT SHOWED IT.
Disabling the byte-identity compare left every arm green -- differing bodies
produce a different key and never reach the compare, so the branch's only
falsifying input is a sha256 collision. It is removed rather than kept as an
unprovable predicate; the full-length key IS condition 3. Disabling the
window left its arm green too, because that arm awaited both requests
sequentially and the leader had already left the map. Rewritten to fire the
second send while the first is still in flight but past the window, and it
now goes red on exactly that mutation.

Gated OFF (CACHE_FIX_COALESCE_SIDECAR). Enabling is a separate declared act:
what a coalesced follower does to duplicate-billing's own measurement is a
decision, not a detail -- with no outcome record the follower reads as the
unanswered first send of a retry streak, which inverts the signal the
mitigation is judged by.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011q6Zo7yKHCGokK4fT8LTPF
One self-contained helper, carried because the test in the previous commit
imports it: every producer gets one run root per process, removed on exit, on
throw, and on SIGINT/SIGTERM/SIGHUP. It deliberately never deletes anything it
did not create, so it is a helper and not a reaper.

Its own header states the two cases it cannot cover — SIGKILL and SIGABRT run
no exit handlers — because a leftover run root then means a child died hard,
which is a finding about that child rather than about this helper. We learned
that the expensive way: three sessions hunted a leak that turned out to be a
test's own deliberate out-of-memory crashes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMiYvNxKq6G9gfJMzArm4q
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 14, 2026
…on because a gate said so

PR cnighswonger#337 carries the coalescer itself, opt-in behind its env gate, with the
measured population in the body — 144 pairs, 114 streaks, 55 billed twice —
and the retry class stated as the design constraint rather than as a caveat,
since suppressing those would leave a real request unanswered.

The record half did not make the slice, and that was decided by the gates
rather than by taste: the cherry-pick conflicted on fork-only paths that do
not exist upstream, and slice-preflight then named five static imports in its
test reaching the census and harvest stack. Ported anyway it would have
widened this PR into the verification stack's topic. The PR body says so, so
the omission is a stated boundary rather than something a reviewer discovers.

Also filed cnighswonger#336, the one-file fix for upstream's hardcoded test port, proven
as a controlled pair with the port deliberately held. Separate PR rather than
a rider, because upstream asked for unrelated changes to be lifted out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMiYvNxKq6G9gfJMzArm4q
@vsits-proxy-builder

Copy link
Copy Markdown
Contributor

Reviewed at f4ca4d1. Load-bearing proxy request-path change; opt-in via CACHE_FIX_COALESCE_SIDECAR=1, off by default.

What's strong

  • Off-by-default gate posture is right for a mechanism whose failure mode changes billing attribution.
  • 15 wire-level tests against a real proxy + real local upstream, not mocks. The mid-session arm asserting TWO upstream calls (not one) is the discriminator — an assertion that only proved "one call for the coalescing case" would pass a build that coalesced everything.
  • Mutation-proof commentary on the byte-equal check (server.mjs:158-163) is exemplary — the author caught that Buffer.equals next to a full-length sha256 was an unfalsifiable predicate whose only red input is a hash collision, and removed it.
  • coalesceCandidate cleanly separates the per-request half of the predicate from the pair-scoped half, and exports it for direct test.
  • Fan-out replay-buffer design (mid-stream attach gets the whole response) is correct and covered.

Blocking

1. Shared-proxy billing/auth leak — not documented.

The coalesce key is sha256(forwardBody) only (server.mjs:151-153). On a shared / multi-tenant proxy, two different users' session-start sidecars are byte-identical (fixed sidecar shape — same model, same max_tokens: 32000, same nMsg=1 no-tools payload). Consequences when the gate is on:

  • Billing: leader's upstream call is charged to leader's account; follower's request is served from the leader's response and never bills follower's account.
  • Auth propagation: if the leader has bad auth (401 from upstream), followers with valid auth still get the leader's 401 (server.mjs:196-198 write to sink, which is the fan-out).
  • Response context: follower receives bytes derived from the leader's plan tier, org, etc.

This is the auth-isolation concern that shared-proxy operators need called out explicitly. The mechanism ships with the right gate posture, but the README has no section for CACHE_FIX_COALESCE_SIDECAR, and the existing shared-proxy warning (README:148, README:1443) talks about crash semantics only.

Two acceptable resolutions, either fine:

  • (a) Add a README section (matching the shape of image-retry / session-budget) documenting the env var, and next to it a warning: "single-tenant only; enabling on a multi-tenant proxy leaks billing attribution and auth-error propagation between users of identical sidecar payloads." Also add one line in the server.mjs comment near the gate check.
  • (b) Include an auth-distinguishing input in the coalesce key (e.g., first N bytes of a stable identity header). Adds one line; removes the operator-warning burden entirely.

I don't have a preference between the two.

Non-blocking suggestions

2. tools/tmpdir.mjs scope. The utility is 201 LOC including staleRunRoots scanning that has no caller in this PR — the author flags this as "self-contained, no other caller in this slice" but the module carries scaffolding (staleRunRoots, RUN_ROOT_PREFIX export, PID-scoped naming for the age-threshold discriminator) for a broader gate-live workflow that isn't in scope here. Trimming to tmpDir + tmpDirSync + their ensureRunRoot / removeRunRoot dependencies would land the same test capability at maybe 80 LOC. The rest can ride with the PR where it's actually called.

3. inFlightSidecars Map has no cap. Entries clean on clientRes.close, but a slow-death path (client hangs, close never fires) leaks entries. Under the gate-off default this is a non-issue; under the gate-on with pathological clients it grows unbounded. A small LRU (say 256) or a timer sweep for entries older than 2×COALESCE_WINDOW_MS would bound it. Not blocking because the gate is off by default and the practical exposure is small.

4. Off-by-default merits a README section anyway. Even for opt-in mechanisms, the repo convention (image-retry, session-budget) is a full section with env-var table and detection conditions. A reader browsing the README won't find this feature without one.

Verdict

Applying changes-requested. Not adding approved-by-code-agent.

Load-bearing per CLAUDE.md, so this will also need Codex review — I'll ask AITL to dispatch once the shared-proxy documentation lands.

— Proxy Builder

@vsits-proxy-builder vsits-proxy-builder Bot added the changes-requested Blocking review findings are outstanding label Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Blocking review findings are outstanding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant