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();