perf(acp): MCP calls over ACP no longer wait behind a long call#23834

open2
main
GitHub fork: r13/b184-let-independent-mcp-over-acp-calls-run-c
opened1d agobyOonly21mil
View on GitHubTwo-way sync enabledPR last imported 1d ago

Problem

When an ACP agent reaches T3's tools through MCP-over-ACP (mcp/connect, mcp/message), every message on a connection went through one lock in mcpOverAcpBridge.ts. The lock covered the whole exchange: the HTTP POST and the full response read. So while one call was pending, such as a t3_thread_wait that can take minutes, every other call on that connection waited, even a tools/list the server could answer at once.

Nothing else in the path is serial. The ACP client's RpcServer and T3's MCP HTTP endpoint (McpServer.layerHttp) both run requests with unbounded concurrency, so the bridge lock was the only thing ordering them.

Change

packages/provider-acp/src/server/mcpOverAcpBridge.ts: send takes the connection lock only until initialize has negotiated a protocol version. After that, messages go straight to fetch and run concurrently.

  • The handshake stays ordered. A message sent before the protocol version is known waits for the lock, as it does today, so nothing is sent ahead of initialize with missing session headers.
  • After the handshake, the session id and protocol version do not change. A response either carries the same session id or none, which keeps the old one, so there is no shared state left to protect.
  • Disconnect is unchanged. It never took the lock. It removes the connection and sends DELETE, and the server interrupts that session's in-flight requests. A late response only updates the removed connection object, so it cannot bring the session back.

Request ids still come from the per-connection counter, and each POST is matched to its own response, as before.

Scope and approval

No issue was filed. I think this fits CONTRIBUTING's exception for a very small, focused fix: a short call waiting minutes behind `t3_thread_wait` is a clear defect, and the fix narrows one lock in one function, with no protocol, setting or UI change. I searched open and closed PRs for this change and found none.

Verification

New test in packages/provider-acp/src/server/mcpOverAcpBridge.test.ts: "answers a call while a long call on the same connection is pending". It initializes a connection, starts a tools/call whose response never arrives until the test releases it, waits until that request reached fetch, then sends tools/list and expects its result. Then it releases the first call and checks its result too. It uses deferred promises, no timers.

  • With main's bridge, tools/list never starts, and the test fails on vitest's 60 s timeout. The other three tests pass.
  • On this branch, all four tests pass.

New test on main and on this branch

For a timing check, I ran a temporary, uncommitted script that drives the real bridge with real fetch against a local HTTP server. Its tools/call answers after 2,000 ms, and tools/list is sent once that call has reached the server. With main's bridge, tools/list took 2,009 ms, the full length of the other call. On this branch it took 1 ms. The slow call took about 2,008 ms in both runs.

Timing on main and on this branch

Also ran: vp lint and vp fmt --check on the two changed files, and tsc-rs --noEmit in packages/provider-acp. The branch test run and both timing runs were on Linux x86_64. The main-side test run was on macOS arm64.

This is server-only. Agents get tool results sooner, and nothing a screen shows changes, so there are no UI screenshots.

Not checked:

  • A live ACP agent that sends concurrent MCP calls on one connection. The repo's mock agent advertises MCP-over-ACP support but never calls it, and I had no provider credentials. The test and the timing script drive the bridge directly.
  • Agents that send several messages before initialize finishes. They still queue one at a time, as they do today.

Prior PRs: none attempted this. #14420 by @kiiwee changes how disconnect discards failed response bodies in this bridge, a different fix in a different function. #14432 by @kiiwee, closed, made the 16-connection cap atomic across concurrent connect calls. This PR does not touch connect.

Production fix, tests, PR description and images written with Claude Opus 5.5 in T3 Code Nightly.

🤖 Generated with Claude Opus 5.5 in T3 Code Nightly

Checks

All checks passed

Added 2 commits

  • perf(provider-acp): run MCP-over-ACP calls concurrently after the handshake1a42091
  • test(provider-acp): answer a call while a long MCP call is pending7921ee7
Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between d7f3b72548a205de52c851a540471607f56ec889 and 1a42091e68cca5b763228d83448cdcbf1b75e49b.

📒 Files selected for processing: 2
  • packages/provider-acp/src/server/mcpOverAcpBridge.test.ts
  • packages/provider-acp/src/server/mcpOverAcpBridge.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📝 Walkthrough

The bridge now uses the connection mutex only while protocol negotiation is incomplete. A test verifies that tools/list can complete while tools/call remains pending on the same connection.

Changes: MCP send concurrency

Layer / File(s)Summary
Negotiated send flow
packages/provider-acp/src/server/mcpOverAcpBridge.ts, packages/provider-acp/src/server/mcpOverAcpBridge.test.ts
send uses the connection mutex while protocolVersion is null and runs without it after negotiation. A test checks that tools/list completes while tools/call remains pending, then resolves the call.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: maria-rcks

Merge Risk: ⚪ Minimal · up to 1a420

No actionable merge-blocking risk was established; the change is ready to merge after normal checks.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Description checkPassedThe description covers the problem, change, scope and approval rationale, verification results, limitations, and agent attribution. It provides specific test and timing evidence.
Title checkPassedThe title is concise, specific, and accurately summarizes the primary performance change: MCP calls over ACP no longer block behind a long-running call.
Linked Issues checkPassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes checkPassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.