mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 17:22:00 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The Paperclip runner runs native OpenCode agents. OpenCode calls Paperclip tools through a local MCP bridge in the OpenCode proxy. OpenCode runs on Bun and uses Bun `fetch` for each MCP request. > - In one chat eval, the model made two parallel tool calls. The bridge produced and wrote both results in about 0.55 s. OpenCode received only one of them. > - The other call waited until Bun's default fetch timeout fired, about five minutes later. The chat turn lost five minutes, and the eval missed its deadline. > - The most likely trigger is the reuse of an idle keep-alive socket. We could not reproduce the loss locally, so this pull request is hardening that is based on a hypothesis. > - This pull request makes the bridge answer every request on its own connection. It also records how each tool result left the bridge, so a recurrence can be attributed from the run log. > - The benefit is that a pooled socket can no longer strand a tool result. If a call is stranded again, the run log shows whether the bridge wrote the answer. ## Linked Issues or Issue Description No public issue exists. Related: Refs #18751 (draft native client acceptance diagnostic), Refs #19546 (abandoned queued calls), Refs #20862 (transport event delivery; it does not touch this path). **What happened** In CI run `gha-42925998380` (chat-features, runner-opencode, `cases.create-case`), the CEO chat run used OpenCode 1.18.34 with an OpenRouter DeepSeek model. At 00:01:12.16 the model issued `list_workflows` (MCP id 3) and `list_projects` (MCP id 4) in parallel. The runner emitted the dynamic tool results for id 4 at 00:01:12.468 and for id 3 at 00:01:12.714 (ok, HTTP 200). The bridge writes the HTTP response directly after that event. There is no queue between them. OpenCode completed `list_projects` at 00:01:13.113. It failed `list_workflows` only at 00:06:01.761 with `The operation timed out.` We read the bundled OpenCode source. Its MCP tool call has no timeout that produces that message: the MCP request timeout gives `-32001 Request timed out`, and the configured value is 1 h for durable calls. With the same binary in Bun mode (Bun 1.3.14), a fetch on a reused keep-alive socket that never gets an answer fails after 298 s with `TimeoutError: The operation timed out.` So Bun's default fetch timeout ended the call, and the client never consumed the written answer. The previous call (`list_agents`) answered at about 00:01:06.89, so its socket was idle for about 5.27 s when the parallel pair started. Our thesis is that call 3 reused that pooled socket, and call 4 opened a new one. **Expected behavior** When the bridge has the result of a tool call, the MCP client gets it. The client must not wait for a five-minute transport timeout. **Steps to reproduce** We did not reproduce it locally. This is the only occurrence in the retained evidence. A macOS stress test (Bun 1.3.14 client, Node 26 server with the bridge's default keep-alive, one call, then an idle time of 4.8–7.2 s, then two parallel calls, 8 pairs in parallel) completed 343 trials and about 1,029 requests with 0 lost results. The failing run was on Linux CI with Node 24. ## What Changed - `mcp-bridge.ts`: every bridge response sends `Connection: close`. The MCP client cannot reuse a pooled keep-alive socket for a later request. A loopback connect per request costs much less than the failure mode. - `mcp-bridge.ts`: each `tools/call` records one closed delivery diagnostic. This includes rejected calls (unsupported tool, invalid input, duplicate identity), which record the outcome `error` (`paperclip.opencode.tool-delivery-diagnostic/v1`) when its response closes. The fields are the call id SHA-256, the socket request index, the socket idle ms, the handler ms, the outcome (`result`, `error` or `unanswered`) and `written` (`writableFinished`). It holds no tool names, inputs or results. The limit is 256 records per bridge. - `opencode-proxy-finish-diagnostics.ts`: the proxy forwards these records to the run log as `warning` frames after session binding. It validates the exact key set and value types and drops anything else. - Tests: a new `mcp-bridge-delivery.test.ts` uses a pooling keep-alive client with the incident's call shape (one call, then two parallel calls). New forwarder cases cover the record validation. The existing finish diagnostic assertion now filters by its own schema. ## Verification - Red: with the `Connection: close` line removed, `mcp-bridge-delivery.test.ts` fails with `expected 'keep-alive' to be 'close'`. The pooling client then reuses the socket. - Green: `cd packages/paperclip-runner && pnpm exec vitest run src/drivers/opencode/mcp-bridge-delivery.test.ts src/drivers/opencode/mcp-bridge-diagnostics.test.ts src/drivers/opencode/mcp-bridge.test.ts src/cli/opencode-proxy-finish-diagnostics.test.ts src/cli/opencode-app-server-proxy.test.ts src/cli/opencode-proxy-abandoned-tool-call.test.ts` gives 6 files and 45 tests passed. - The new test asserts that all three results arrive with the correct JSON-RPC ids, that no socket is reused, and that each call records `socketRequestIndex: 1`, `outcome: "result"`, `written: true` without tool names or raw ids. A second test asserts that a client that leaves before its answer records `outcome: "unanswered"`, `written: false`. A third test asserts that a rejected call records `outcome: "error"`, `written: true`. - `tsc -p tsconfig.json --noEmit` for `packages/paperclip-runner` reports no errors in the changed files. Seven existing errors in `codex-runtime-adapter.ts` and `eval/workflow-harness.ts` come from this worktree's linked dependencies. Required CI typecheck is the source of truth. - This does not prove the production fix. A recurrence on CI will show socket reuse, idle time and `written` for the stranded call in the run log. ## Risks - Low risk. Each MCP request now opens a new loopback TCP connection. That adds well under 1 ms per call, and there is no TLS. - The `notifications/cancelled` handling and the abandoned-call abort do not change. The abort still reads `writableFinished` on response close. - One extra run-log warning for each tool call (at most 256 for each bridge). The record holds only a hash, counters and timings. - Previous-release story (upgrade-safe run state): no persisted run, session, launch or pack state changes. A bridge from the previous release keeps its behavior until the proxy restarts. - If the thesis is wrong, the five-minute stall can recur. The new records will then show `written: true` on a fresh socket and point the next investigation at OpenCode or Bun. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with 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 <noreply@anthropic.com>