docker / docker/docker-agent

transfer_task is not concurrency-safe: parallel calls race on current-agent pointer, later calls fail with "No agents are configured in this list"

Open
#4,156 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area/agent area/runtime
Dominant language
Go
Stars
3.3k
Forks
462
Avg merge
1d 10h
Merged PRs (30d)
273

Description

Symptom

When a model issues multiple transfer_task calls in one API response, Dispatcher.Process runs them in parallel goroutines (via concurrent.MapSlice). The first goroutine's swapCurrentAgent sets r.current to the target agent (e.g. "drafter"); subsequent goroutines' handleTaskTransferresolveSessionAgent read the mutated r.current, treat the target as the caller, and fail validation:

Agent drafter cannot transfer task to drafter: target agent not in sub-agents list. No agents are configured in this list.

Real-World Impact

Broke the docker-agent-action PR review pipeline on docker/frontends#16712:

Root Cause

Concurrent mutation of r.current:

Thread 1 (call 1):
  a = r.resolveSessionAgent(sess) → r.current = "orchestrator" → OK
  validateAgentInList(orchestrator, "drafter") → PASS
  runForwarding() → swapCurrentAgent: r.current = "drafter"  ← mutated

Thread 2 (call 2, concurrent):
  a = r.resolveSessionAgent(sess) → r.current = "drafter"    ← reads mutated value
  validateAgentInList(drafter, "drafter") → FAIL

The shared mutable field r.current is not safe for concurrent swapCurrentAgent + handleTaskTransfer calls. The caller agent identity must be captured before any concurrent dispatch, not re-read during validation.

Affected Versions

At least v1.120.0–v1.131.0. The parallel dispatch via concurrent.MapSlice (and hence the race) pre-exists v1.121.0; no fix found in v1.121.0..v1.131.0 history.

Suggested Fixes

  1. Store caller agent on session creation: Record the "owner agent" in the session object at creation time; resolveSessionAgent reads the session-owned agent for root sessions, never r.current.
  2. Snapshot caller identity before dispatch: Capture the caller agent at the top of handleTaskTransfer before any concurrent dispatch; pass it through to validation (do not re-read r.current).
  3. Serialize transfer_task in dispatcher: Add a mutex that serializes all transfer_task calls; simplest runtime fix but less elegant.

Workaround

Set parallel_tool_calls: false on the model in your agent config. This prevents the model from returning multiple transfer_task calls in a single API response, eliminating the race entirely.

Key Code References

  • pkg/runtime/toolexec/dispatcher.go:257concurrent.MapSlice(calls, ...) parallel dispatch
  • pkg/runtime/agent_delegation.go:681handleTaskTransfer reads r.current via resolveSessionAgent
  • pkg/runtime/agent_delegation.go:326runForwarding / swapCurrentAgent mutates r.current
  • pkg/runtime/agent_router.go:76ResolveSession reads r.current for unpinned sessions

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with pkg/runtime/toolexec/dispatcher.go:257 and trace the parallel calls into handleTaskTransfer at pkg/runtime/agent_delegation.go:681. Read runForwarding and swapCurrentAgent at agent_delegation.go:326, then inspect ResolveSession in agent_router.go:76. Reproduce the reported parallel transfer_task scenario and verify that concurrent calls retain the original caller identity without validation failures.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, distributed-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
54/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.