Files
PaperClipAI/packages
DottaandClaude Opus 5.5 e601f9d2fc fix(runner): answer each OpenCode MCP tool call on its own connection (#20886)
## 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>
2026-11-11 12:29:59 -06:00
..