Repository navigation
fix: shrink withSessionLock to state-mutating sections (#970) - #972
Conversation
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-972Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr972.tgz
npm install -g packageInstall instructions are refreshed on each push. |
[bot] 🏷 Received — this is the follow-up to the #970 lock-scope fix (already triaged + implemented in-thread there). Verifying now: pulling the branch, reviewing the diff against current master, re-running the full pre-flight (typecheck / tests / build) locally, and checking CI. Will report the verdict shortly. |
[bot] 🏷 Review complete — verified, CI fully green, ready to merge. This is the #970 lock-scope fix from my earlier session; per protocol I did not redo it but independently re-verified every claim. Verification performed (all on head
Verdict: scope is right (fixes the lock-chain mechanism, leaves the three out-of-scope items for your decision), both compression modes reasoned, evidence is real. No changes needed — merge when you're ready. (Merging itself stays with you: I can't merge PRs.) 中文摘要:独立复核了锁缩小方案(所有跨 await 的状态变更段均已加锁、只读工具路径为既有同步实现、回归测试在旧代码上确实失败/新代码上通过),本地 typecheck/全量测试(1900 通过)/构建全部复跑通过,CI 八项检查(含真实 codex E2E)全绿,无需修改,可以合并。 |
…requests on one session stop head-of-line-blocking (#970)
9462eb5 to
16dac72
Compare
Closes #970
What changed
withSessionLockwas held fromprepare()through the entire upstream response stream (src/server.ts). Every concurrent request sharing a session id — Claude Code's main turn plus all background subagents, which do not carry distinct session ids — serialized behind one slow upstream stream, producing the 1-2+ minute first-token stalls in #970.The lock now covers only the fast state-mutating sections:
src/server.ts): prepare + preflight only; returns{body, prepared}and releases beforeforward()runs.forward(), each discrete mutation re-acquires the lock just long enough:markNativeCompactionBoundary(native compact completion)refreshFolded(post-compressprocessTurn+session.state =)executeProxyToolin the streaming compress loop (src/loop/core.ts) and the Responses JSON path (src/compress-loop-responses.ts)rememberPluginMessagesafter forwardMUTATING_PROXY_TOOLSand run synchronously, hence atomically). The plugin-MCP tool path was already locked.withSessionLocksignature widened to accept sync callbacks (src/session.ts).Net effect: a slow upstream delays only its own request; requests queued behind it proceed concurrently.
Why this shape
The lock exists to protect
session.statefrom interleavedprocessTurn/ stream-rewriter mutations. On Node's single-threaded runtime, only sections spanning anawait/yield suspension point can interleave — exactly the sections locked above. Wire shape is unchanged: same outbound bodies, same streams, same injection points, reasoned in both compression modes (full suite + real-client E2E below).Regression test
tests/session-lock-concurrency.test.ts: two concurrent Anthropic-wire requests on one session against a mock upstream holding its first byte for 400ms. Asserts the second upstream arrival lands within half the hold window (pre-fix code waited the full ~400ms), plus state integrity (message-ref ids never re-issued under concurrent prepares) and a follow-up sequential request. Verified to FAIL on pre-fixsrc/and pass with the fix.Pre-flight (rebased onto current master)
npm run typecheck— cleannpm test— 1900 pass / 0 fail / 2 skippednpm run build— successtests/e2e/e2e-codex-fake.test.ts, deterministic fake Responses upstream): overflow scenario (compression really fires inside real codex, bulk folded, sentinels retained) + under-window control (no compression) — both pass.Out of scope (remaining items from #970, need owner decision)
This PR fixes the lock-chain mechanism that turned those slow first bytes into head-of-line blocking for every request on the session.