Skip to content

fix(adapters): persist the MCP lane before the agent can see the response - #279

Merged
AminChirazi merged 3 commits into
mainfrom
fix/mcp-lane-durability
Jul 31, 2026
Merged

fix(adapters): persist the MCP lane before the agent can see the response#279
AminChirazi merged 3 commits into
mainfrom
fix/mcp-lane-durability

Conversation

@AminChirazi

Copy link
Copy Markdown
Contributor

Fixes the false green found by the falsifiability suite. Closes #273 and #257, and supersedes draft PR #274 (which was the evidence).

The defect

0.8.0 fixed the stand-in losing its whole recording on an abrupt kill, by persisting the lane after every captured call. It was not enough.

The stand-in forwarded the server's response to the agent before persisting it (mcp_stdio.rs:363, capture at :416). The moment the agent has a response it may kill the stand-in — so the final call of a session was lost, while record still exited 0.

With the real server's own request log as the oracle:

Server received initialize, notifications/initialized, tools/list, tools/call get_weather
Lane recorded initialize, tools/list

A lane that silently runs short understates what the agent did, and every assertion over it inherits the omission — including a guard claim that a tool was never called.

The fix, both halves

1. Ordering. Capture and persist happen before the response is forwarded. By the time the agent can act on a response, the evidence for it is on disk. The mocked-tool path already had this ordering and never raced; only the forwarded path did.

2. Detectability. A failed flush is no longer swallowed. The old code ignored it deliberately, reasoning that the write at stdin EOF might still save the run — but that write is exactly what an abrupt kill skips, so it was never a fallback. A flush that fails now fails the record by name rather than minting a partial lane.

That second half is the one that matters beyond this instance: it turns "the lane ran short" from silent into loud.

Verified

Ships with the test that found it, cherry-picked from the evidence branch unchanged — it failed on the old code and passes here. No adjusting the test to fit the fix.

falsifiability_mcp_kill pass (was the failing evidence)
mcp_stdio_e2e 6 pass
mcp_http_e2e 1 pass
flowproof-adapters pass
fmt --check / clippy -D warnings 0 / 0

Clippy caught a second flush_lane call site whose Result I had left unhandled — the exact swallowed-error case this change exists to remove.

Note

#274 can be closed once this merges; its content is here, and its test now passes.

🤖 Generated with Claude Code

claude added 3 commits July 31, 2026 04:22
…stand-in

EVIDENCE, NOT A FIX. This test is expected to FAIL. Do not merge.

0.8.0 fixed "the MCP stand-in no longer loses its recording when the agent
kills it" -- the lane had been written only at stdin EOF, so an abrupt kill
lost everything. The lane is now persisted after every captured call.

It is not enough. An agent that kills the stand-in immediately after reading
a response loses THAT call from the lane, while every earlier call survives.

Observed, with the real server's own log as the oracle:

  server received:  initialize, notifications/initialized, tools/list,
                    tools/call get_weather
  lane recorded:    initialize, tools/list

`record` exited 0 and minted a trace. So a tool the agent demonstrably
invoked is absent from the evidence, and nothing said so.

That is the defect class this milestone exists to eliminate, in the artifact
the MCP boundary's claims rest on. A lane that silently runs short understates
what the agent did, and every assertion evaluated over it inherits the
omission.

The fixture is an agent identical to the suite's polite one but for its last
act: proc.kill() instead of stdin.close(); wait(). Every existing MCP test
closes politely, which is exactly the path the 0.8.0 defect did not affect.

Reported rather than fixed: the brief forbids self-authorising a fix for a
defect the same run discovered.

Refs #257

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formatting only. The single intended CI failure on this branch is the
assertion that documents the finding, not a lint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onse

0.8.0 fixed the stand-in losing its whole recording when an agent killed it,
by persisting the lane after every captured call. It was not enough.

The stand-in forwarded the server's response to the agent BEFORE persisting
it. The moment the agent has a response it may kill the stand-in, so the
final call of a session was lost -- while `record` still exited 0. A tool the
agent really invoked was simply absent from the evidence, silently.

Found by the falsifiability suite (#273), with the real server's own request
log as the oracle: the server was asked for `tools/call get_weather`, the
lane held only `initialize` and `tools/list`, and nothing said so.

That matters more than a lost line. A lane that silently runs short
UNDERSTATES what the agent did, and every assertion evaluated over it
inherits the omission -- including a guard claim that a tool was never
called.

Capture and persistence now happen before the response is forwarded. By the
time the agent can act on a response, the evidence for it is on disk. The
mocked-tool path already had this ordering and never raced; only the
forwarded path did.

And a failed flush is no longer swallowed. The old code deliberately ignored
it, on the reasoning that the write at stdin EOF might still save the run --
but that write is exactly what an abrupt kill skips, so it was never a
fallback. A flush that fails now fails the record by name rather than minting
a partial lane. Evidence lost silently is the one outcome this tool cannot
ship.

Ships with the test that found it, cherry-picked from the evidence branch
unchanged: it failed on the old code and passes here.

Closes #273
Closes #257

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 31, 2026 04:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@AminChirazi
AminChirazi merged commit 0a463ab into main Jul 31, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[false-green] The MCP lane loses the final call when the agent kills the stand-in

3 participants