fix: make transfer_task concurrency-safe under parallel tool calls - #4180
Open
bernard-code-lab wants to merge 1 commit into
Open
fix: make transfer_task concurrency-safe under parallel tool calls#4180bernard-code-lab wants to merge 1 commit into
bernard-code-lab wants to merge 1 commit into
Conversation
Collaborator
|
👋 Some commits in this PR are not signed and verified by GitHub. Please sign your commits with a GPG or SSH key registered in your GitHub account, then force-push. Commits that are not verified: See GitHub's guide on signing commits for setup instructions. I've added |
When a model issues several transfer_task calls in one response, the dispatcher runs them in parallel goroutines. Each one re-resolved its caller from the shared current agent that a sibling's swapCurrentAgent had already mutated, so the later calls validated the target against the wrong caller and failed with "target agent not in sub-agents list", and a child that did start could run its turns as another sibling's agent. Resolve the caller from the snapshot Dispatcher.Process already takes before the fan-out, exposed on the per-call tools.Runtime handle, and let only one delegation at a time own the shared current agent — siblings that lose the claim pin their child session instead, the isolation mode background delegations already use. run_skill shares runForwarding and had the same latent defect when it landed in a batch alongside transfer_task; it is fixed by the same change. run_background_agent resolves its caller the same way, but agenttool's HandleRun does not receive a tools.Runtime, so that path is left for a follow-up. Its children are already pinned, which bounds the impact. Fixes docker#4156 Signed-off-by: bernard-code-lab <rafaelc869@gmail.com>
bernard-code-lab
force-pushed
the
fix/4156-concurrent-transfer-task
branch
from
September 7, 2026 00:24
7b3dc36 to
b6fdcaf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4156
Problem
When a model issues several
transfer_taskcalls in one API response,Dispatcher.Processruns them in parallel goroutines viaconcurrent.MapSlice. Each goroutine re-resolved its caller throughresolveSessionAgent, which reads the sharedagentRouter.currentthat a sibling'sswapCurrentAgenthad already mutated:The reported symptom is the loud one:
There is a quieter consequence too.
runForwardingonly pins the child session when the parent is already pinned, so a foreground delegation's child stays unpinned andloop.gore-resolves its agent fromcurrenton every iteration. With the deferred restores interleaving, a child could execute turns with another sibling's identity, toolset and model — no error, wrong output.validateDelegationinherits the same wrong caller, which can also produce a spuriousdelegation cycle detected.atomic.Pointerkeepscurrentfree of data races, but it cannot make the read → validate → mutate → read sequence serializable.Fix
Two orthogonal changes.
1. Resolve the caller from the batch snapshot.
Dispatcher.Processalready resolves the caller exactly once, before the fan-out, and stores it on thecallthat backs every handler'stools.Runtime. It just was not reachable.callRuntimenow exposes it, andLocalRuntime.callerAgentprefers it over session resolution, falling back for hosts that provide no snapshot. No handler signature changes —rt tools.Runtimewas already a parameter ofToolHandlerFunc.2. Let one delegation own the shared current agent.
runForwardingclaims it with a CAS; a sibling that loses the claim pins its child instead of stomping the swap. This is the same isolation mode background delegations already use.run_skillsharesrunForwardingand had the same latent defect when it landed in a batch alongsidetransfer_task; the same change fixes it.Behavior change
With a single
transfer_taskin the batch — the common case — behavior is unchanged: the swap happens, the child stays unpinned, and the existing foreground tests pass untouched.With N parallel transfers, the 2nd..Nth child is pinned. Pinned children do not take the
handoff/force_handoffpath (loop.goguards onsess.AgentName == ""). That is a pre-existing property of pinning, now reachable in a new scenario, and it is documented onSwitchCurrentAgent. If you would rather have no variance between the single and parallel cases, the alternative is to pin every child whenever the batch holds more than one delegation — same trade, but it gives up the untouched single-transfer path.Out of scope
run_background_agentresolves its caller the same way, butagenttool.Handler.HandleRundoes not receive atools.Runtime, so fixing it means changing a signature outsidepkg/runtime. Left for a follow-up; its children are already pinned, which bounds the impact.Testing
Two regression tests, both verified to fail with the source changes reverted:
TestTransferTask_UsesBatchCallerSnapshotNotSharedCurrentAgent— deterministic, reproduces the exact error string from the issue.TestTransferTask_ConcurrentForegroundTransfersStayIsolated— end-to-end throughRunStream→Dispatcher.Process→concurrent.MapSlice, with threetransfer_taskcalls in one assistant response. Without the fix two of the three children return no output and the race detector fires on a shared test stream, which is the cross-wiring made visible: two children had resolved to the same agent. With the fix it is stable over 20 runs under-race.task lint/task testwere run as their underlying commands, sincetaskis not installed locally.