Skip to content

fix(claude_agent_sdk): prevent orphan spans for racing MCP tools - #689

Merged
Abhijeet Prasad (AbhiPrasad) merged 2 commits into
mainfrom
abhi/sdk-225-claude-agent-sdk-integration-creates-orphan-duplicate-tool
Aug 14, 2026
Merged

fix(claude_agent_sdk): prevent orphan spans for racing MCP tools#689
Abhijeet Prasad (AbhiPrasad) merged 2 commits into
mainfrom
abhi/sdk-225-claude-agent-sdk-integration-creates-orphan-duplicate-tool

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Member

resolves https://linear.app/braintrustdata/issue/SDK-225/claude-agent-sdk-integration-creates-orphan-duplicate-tool-spans-when

Local MCP handlers could run before the assistant tool_use reached the application-consumed stream, creating duplicate root traces and attaching nested work or errors to the wrong span. This cluttered projects and made tool failures hard to diagnose.

Observe raw SDK messages before control dispatch so handlers re-enter the canonical nested tool span without an arbitrary wait. Add deterministic cassette-backed race coverage.

Local MCP handlers could run before the assistant tool_use reached the application-consumed stream, creating duplicate root traces and attaching nested work or errors to the wrong span. This cluttered projects and made tool failures hard to diagnose.

Observe raw SDK messages before control dispatch so handlers re-enter the canonical nested tool span without an arbitrary wait. Add deterministic cassette-backed race coverage.

@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: ad4ccab858

ℹ️ 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 (@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 (@codex) address that feedback".

Comment on lines +1063 to +1064
if request_tracker is None:
request_tracker = getattr(_thread_local, "request_tracker", None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind raw messages to the originating one-shot query

When two exported claude_agent_sdk.query() iterators run concurrently on the same event-loop thread, each RequestTracker overwrites this threading.local() slot, while their internal Query objects have no _braintrust_request_tracker binding. A message emitted by the first query can therefore be pretraced into the second query's tracker; subsequent type-based suppression then produces duplicated spans in one trace and missing or cross-request content in the other. Store the tracker on the originating Query or use request-scoped async context rather than the last tracker on the thread.

Useful? React with 👍 / 👎.

Comment on lines +269 to +270
active_tool_span = tool_span_tracker.acquire_span_for_handler(tool_name, args)
if active_tool_span is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retain fallback tracing for an unrelated active tracker

With two wrapped clients active concurrently on one event-loop thread, _thread_local.tool_span_tracker belongs to whichever request was created last. If the earlier client's handler uses a different tool name or input, this lookup returns an unrelated non-None tracker but acquire_span_for_handler() returns None; the new branch then runs the handler without either its canonical tool span or the previous fallback span, so nested work becomes orphaned. The presence of any thread-local tracker is not sufficient to suppress fallback tracing unless it is known to own this handler invocation.

Useful? React with 👍 / 👎.

Bind raw SDK readers to request-owned trackers without retaining ContextVar tokens across async generator yields. Restore fallback tracing for unmatched local handlers and add deterministic cassette coverage for concurrent clients, query helpers, cancellation, and cross-context cleanup.
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit edda629 into main Aug 14, 2026
171 of 173 checks passed
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) deleted the abhi/sdk-225-claude-agent-sdk-integration-creates-orphan-duplicate-tool branch August 14, 2026 15:22
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