From 64afd22d828de38f19154f92e904f06cfbff674e Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 11 Nov 2026 14:10:56 -0600 Subject: [PATCH] fix(runner): keep the OpenCode MCP bridge on keep-alive so OpenCode keeps its Paperclip tools (#20921) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Native OpenCode agents call Paperclip tools through a local MCP bridge in the runner's OpenCode proxy > - #20886 made the bridge send `Connection: close` on every response, as hardening for one stalled parallel tool call that was never reproduced > - After #20886 merged, many native OpenCode runs started without any Paperclip MCP tools > - Agents then said the Paperclip tools were missing, worked only with bash and file tools, and never called paperclip_finish > - This pull request removes the `Connection: close` header and keeps the delivery diagnostics from #20886 > - The benefit is that OpenCode agents get their Paperclip tools again, and a recurrence of the original stall still shows in the run log ## Linked Issues or Issue Description Refs #20886. **What happened** In chat eval runs on CI, every OpenCode session before #20886 sent a first model request of about 34k–36k input tokens (prompt plus the Paperclip tool schemas). After #20886, about 9 of 25 sessions sent only about 8k–15k tokens on the same prompts (for example 14,711 for questions-and-approvals.necessary-question, 15,481 for cases.cases-disabled, 14,096 for cases.update-case-facts). The prompt envelope size did not change (about 32.7k characters). Those sessions made no Paperclip MCP calls: zero tool-delivery records, no paperclip_finish. The agents' own reasoning said "The paperclip tools aren't in my toolset". Sessions on a commit without #20886 (8d2e34eac1) all had the full size. **Expected behavior** Every native OpenCode session starts with the Paperclip MCP tools. **Steps to reproduce** 1. Run several chat-features OpenCode cells on master after #20886. 2. Compare the first usage record of each run (inputTokens plus cacheReadTokens) and the tool names in the run events. 3. See sessions with about 15k tokens and no paperclip_* tool calls. ## What Changed - `mcp-bridge.ts`: removed `response.setHeader("Connection", "close")`. The bridge uses Node's default keep-alive again, as before #20886. - The per-call delivery diagnostics from #20886 stay, so a recurrence of the stalled parallel call still shows socket reuse, idle time and `written` in the run log. - `mcp-bridge-delivery.test.ts`: the pooled-client test now asserts that the bridge does not close connections, that a later call reuses a socket, and that each call still gets a correct delivery record. ## Verification - `pnpm exec vitest run` for mcp-bridge-delivery, mcp-bridge, mcp-bridge-diagnostics, opencode-proxy-finish-diagnostics, opencode-app-server-proxy and opencode-proxy-abandoned-tool-call tests: 45 passed. - The exact mechanism inside OpenCode/Bun is not reproduced locally (a macOS Bun client against a `Connection: close` server completed 300 MCP-shaped handshakes without error). The evidence is the before/after correlation above. The next chat eval runs will show the first-request token counts again. ## Risks - The original five-minute stall (one occurrence, cause not proven) can recur. The diagnostics from #20886 will then name the socket state. - No persisted run, session or pack state changes. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution in Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- .../drivers/opencode/mcp-bridge-delivery.test.ts | 13 ++++++++----- .../src/drivers/opencode/mcp-bridge.ts | 9 ++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/paperclip-runner/src/drivers/opencode/mcp-bridge-delivery.test.ts b/packages/paperclip-runner/src/drivers/opencode/mcp-bridge-delivery.test.ts index 74a7742792c..d2543e2ce9e 100644 --- a/packages/paperclip-runner/src/drivers/opencode/mcp-bridge-delivery.test.ts +++ b/packages/paperclip-runner/src/drivers/opencode/mcp-bridge-delivery.test.ts @@ -42,7 +42,7 @@ const tools = [ { name: "list_projects", inputSchema: { type: "object" } }, ]; -it("answers every tool call on its own connection so a pooled socket can never strand a parallel result", async () => { +it("answers each tool call, including a parallel pair on a pooled keep-alive client, with its own delivery record", async () => { const messages: string[] = []; const bridge = await startOpenCodeMcpBridge({ tools, @@ -69,8 +69,9 @@ it("answers every tool call on its own connection so a pooled socket can never s expect(answer.status).toBe(200); expect(answer.body.id).toBe(id); expect(JSON.parse(answer.body.result!.content![0]!.text)).toEqual({ tool }); - expect(answer.connection).toBe("close"); - expect(answer.reusedSocket).toBe(false); + // OpenCode's MCP client loses its tools when the bridge closes every + // connection, so the bridge must keep the default keep-alive. + expect(answer.connection).not.toBe("close"); } await expect.poll(() => messages.length).toBe(3); @@ -78,12 +79,14 @@ it("answers every tool call on its own connection so a pooled socket can never s expect(deliveries.map(delivery => delivery.callIdSha256).sort()).toEqual( ["2", "3", "4"].map(id => createHash("sha256").update(id).digest("hex")).sort(), ); + // The pooled client reuses a socket for at least one later call. + expect(deliveries.some(delivery => delivery.socketRequestIndex > 1)).toBe(true); for (const delivery of deliveries) { expect(delivery).toEqual({ schema: "paperclip.opencode.tool-delivery-diagnostic/v1", callIdSha256: expect.stringMatching(/^[a-f0-9]{64}$/), - socketRequestIndex: 1, - socketIdleMs: null, + socketRequestIndex: expect.any(Number), + socketIdleMs: delivery.socketRequestIndex === 1 ? null : expect.any(Number), handlerMs: expect.any(Number), outcome: "result", written: true, diff --git a/packages/paperclip-runner/src/drivers/opencode/mcp-bridge.ts b/packages/paperclip-runner/src/drivers/opencode/mcp-bridge.ts index 2dc6c73cf05..8cfbe33a303 100644 --- a/packages/paperclip-runner/src/drivers/opencode/mcp-bridge.ts +++ b/packages/paperclip-runner/src/drivers/opencode/mcp-bridge.ts @@ -242,11 +242,10 @@ async function handleRequest( }, ): Promise { setSecurityHeaders(response); - // Never let the MCP client reuse a pooled socket for a later request. A - // native OpenCode run (Bun fetch) once lost a written tools/call result on - // a parallel call and waited about five minutes for Bun's fetch timeout. - // A loopback connect per request costs far less than that failure mode. - response.setHeader("Connection", "close"); + // Keep the default keep-alive. Forcing `Connection: close` here left + // OpenCode (Bun fetch) without the Paperclip MCP tools in many native runs: + // first model requests dropped from ~35k to ~15k input tokens (no tool + // schemas) and the agents reported that Paperclip tools were missing. if (!authorized(request.headers.authorization, context.secret)) { response.statusCode = 401; response.end();