Skip to content

fix(session runner): retry the sub-thread tool_use commit-race 400 instead of stranding (#1744) - #1753

Open
nadavshohat wants to merge 1 commit into
anthropics:mainfrom
nadavshohat:fix/subthread-tool-use-race-1744
Open

fix(session runner): retry the sub-thread tool_use commit-race 400 instead of stranding (#1744)#1753
nadavshohat wants to merge 1 commit into
anthropics:mainfrom
nadavshohat:fix/subthread-tool-use-race-1744

Conversation

@nadavshohat

Copy link
Copy Markdown

Mitigates #1744. Not a root-cause fix — the 400 is generated server-side (the sub-thread agent.tool_use event isn't matchable yet when the result is posted), so the real fix has to happen there. This is the client half.

The bug

When a tool runs inside a non-primary session_thread_id (a sub-agent thread), posting its user.tool_result intermittently returns 400 … does not match any agent.tool_use event, even though the tool ran and the same tool succeeds on other attempts. Today SessionToolRunner._send_result classifies that 400 as fatal via is_fatal_status_error and bails on the first attempt, so a transient commit race becomes a permanently stranded tool call — the session then waits forever on that final pending result and never delivers.

The change

Recognize that specific 400 and retry it on a more patient budget (SUBTHREAD_RACE_SEND_RETRIES = 8, backoff capped at STREAM_BACKOFF_CAP) so the result lands once the event commits, instead of giving up. Everything else is unchanged:

  • Scoped to _send_result only — the shared is_fatal_status_error is untouched, so no other retry path (stream loop, poller) changes behavior.
  • Every non-race 4xx still bails immediately (a regression test covers a generic 400 still being fatal).
  • Matched on the error message because the API exposes no distinct error code for this case. Happy to key off a proper code if one exists, or move the predicate into lib/_retry.py if you'd prefer it next to the other classification.

Residual

The extended budget covers the common case (the event commits within the retry window). A pathologically long race can still exhaust it, at which point the call is surfaced as before — that final gap needs the server-side fix.

Tests

Three deterministic tests in tests/lib/tools/test_session_runner.py: the race 400 is retried and posts, it retries past SEND_RETRIES, and a non-race 400 still bails. ruff + full test_session_runner.py green.

Open question for a maintainer (same as #1744)

Is a client meant to be able to confirm a sub-thread agent.tool_use, and is there a readiness signal we should wait on rather than retry blindly? If the intended fix is server-side, this can stand as a stopgap or close in favor of it.

@nadavshohat
nadavshohat requested a review from a team as a code owner July 11, 2026 20:45
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.

1 participant