Skip to content

Fix MCP session lifecycle and replay safety - #2134

Open
xeophon wants to merge 3 commits into
mainfrom
agent/fix-mcp-session-lifecycle
Open

Fix MCP session lifecycle and replay safety#2134
xeophon wants to merge 3 commits into
mainfrom
agent/fix-mcp-session-lifecycle

Conversation

@xeophon

@xeophon xeophon commented Jul 26, 2026

Copy link
Copy Markdown
Member

Overview

Centralize null and bash harness MCP handling into one lifecycle-aware client and make transport failures explicit, replay-safe, and diagnosable.

Details

  • own the MCP transport in a dedicated task so SDK cancellation scopes stay with their owner
  • preserve outer cancellation while bounding cancellation-time session teardown
  • distinguish replay-safe connection failures from calls whose delivery is unknown
  • prevent automatic replay when a non-idempotent tool call may have reached the server
  • emit structured diagnostics for the parent harness and retry policy
  • remove the duplicated null-harness MCP client by sharing the null and bash program

Note

Medium Risk
Changes MCP failure semantics and default retry behavior (delivery-unknown failures no longer replay unless opted in), and consolidates null/bash agent programs—important for harness rollouts but not auth or payment paths.

Overview
Centralizes MCP handling for null and bash harnesses into a shared embedded mcp_client.py, replacing duplicated inline logic and deleting the null-only program.

The new client runs the MCP transport in a dedicated task (so SDK cancellation scopes stay isolated), classifies transport loss as transient vs not and replay-safe vs not, and emits VF_MCP_ERROR= JSON diagnostics. Harness.run parses those lines and raises MCPTransportError or MCPDeliveryUnknownError instead of a generic HarnessError. In-process retries only fire when failures are marked session_retry_safe; call_tool uses replay_safe=False so a lost response is not automatically replayed.

Null now runs the same bash program.py without --bash (MCP-only chat, 60s enumeration timeout); bash passes --bash for local tools and unbounded MCP setup. trace_should_retry / episode_should_retry block whole-rollout replay on MCPDeliveryUnknownError unless RetryConfig explicitly includes that type.

Reviewed by Cursor Bugbot for commit 552f444. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix MCP session lifecycle and add replay-safe retry policy for MCP transport failures

  • Adds a new mcp_client.py with a robust MCP session manager that isolates the HTTP transport in its own task, handles cancellation/shutdown, and raises a structured MCPTransportFailure exception with JSON metadata (delivery status, replay safety, operation phase) on failure.
  • Introduces two new error classes MCPTransportError and MCPDeliveryUnknownError in errors.py; the harness parses VF_MCP_ERROR= output from the program and raises the appropriate error.
  • Adds retry logic in mcp_client.py via with_retry that retries transient transport failures only when the operation is replay-safe (e.g. list_tools but not call_tool).
  • Consolidates the null and bash harness programs into a single shared source, injecting the MCP client at build time via a # <vf:mcp-client> marker; null harness drops its own program.py.
  • Extends trace_should_retry in retries.py to veto episode retries when MCPDeliveryUnknownError is present unless the retry policy explicitly opts in.
  • Behavioral Change: episodes where an MCP tool call response status is unknown will no longer be retried by default, preventing duplicate non-idempotent operations.

Macroscope summarized 552f444.

Comment thread verifiers/v1/retries.py Outdated
Comment thread verifiers/v1/harnesses/mcp_client.py
@macroscopeapp

macroscopeapp Bot commented Jul 26, 2026

Copy link
Copy Markdown

Approvability

Verdict: Needs human review

Unable to check for correctness in 552f444. This PR introduces substantial new MCP session lifecycle management code (~300 lines) with complex async cancellation handling, new public error types, and changes to retry behavior that gate when operations can be replayed. The scope and complexity warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67e0acc36c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread verifiers/v1/retries.py Outdated
@xeophon
xeophon force-pushed the agent/fix-mcp-session-lifecycle branch from fffc408 to 18109b8 Compare July 27, 2026 13:28
@xeophon
xeophon requested a review from mikasenghaas July 27, 2026 13:30

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 18109b8. Configure here.

Comment thread verifiers/v1/harnesses/mcp_client.py

@mikasenghaas mikasenghaas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we have some data/ experiments to backup the robustness improvs around mcp?

MCP_CALL_ATTEMPTS = 6
MCP_TIMEOUT = httpx.Timeout(600.0, connect=5.0) # the OpenAI SDK client defaults

# <vf:mcp-client>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

whats this?

parser.add_argument("--prompt", default="")
parser.add_argument("--initial-messages-file", default="")
parser.add_argument("--mcp-config", default="")
parser.add_argument("--bash", action="store_true")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is this optional now?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ohh i see, i can get behind it. but maybe we should mark shared utils in the filetree a bit clearer. myb:

  • v1.harnesses.minimal.mcp -> mcp client stuff
  • v1.harnesses.minimal.program -> shared program (imported by bsah + null)

Comment on lines +42 to +62
transient = (
isinstance(
leaf,
(
asyncio.CancelledError,
BrokenResourceError,
ClosedResourceError,
EndOfStream,
httpx.TransportError,
OSError,
TimeoutError,
),
)
or (
isinstance(leaf, McpError)
and leaf.error.code == CONNECTION_CLOSED
and leaf.error.message == "Connection closed"
)
or status in (408, 429)
or (status is not None and status >= 500)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this looks a bit wild

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.

2 participants