[BUG] Agent Teams: lead session loops on idle notifications and duplicate task_assignment echoes, burns ~13–22% of input tokens on no-op acks
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When orchestrating a team of 5–8 teammates via Agent Teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1), the lead session gets wedged into an acknowledgement loop that costs a full lead turn for every notification:
- Idle-notification turns. After every teammate turn ends, the lead receives a
{"type":"idle_notification","from":"dev-N","idleReason":"available"}message as a fresh turn. For 8 teammates completing 2–3 turns each, this produces 20–40 lead-turn firings whose entire work is "acknowledge idle." - Duplicate
task_assignmentechoes. Teammates report receiving stale/self-originating dispatches for tasks they've already completed. Each echo triggers a teammate turn → a new completion message → another lead turn → another idle notification loop. One teammate wrote:"Received a task_assignment for #3 that appears to be from my own agent ID (likely an echo/stale queue message) — ignoring." - Lead never gracefully stops reacting. Even after every task is marked completed, idle notifications keep arriving for several minutes. The lead keeps answering each one.
Net effect: a piece of coordination work that should take ~6–8 lead turns takes 60–100. Token consumption scales with the product (teammates × notifications), not with actual work.
Token-cost evidence from a real session
I instrumented the transcript JSONL of a single session that orchestrated 8 teammates (1× TeamCreate + 5 TaskCreate initial + 3 incremental tasks, all tasks completed, no rework). Classified every lead turn by what message triggered it:
| Trigger | Turns | Input tokens | % of teams-portion |
|---|---:|---:|---:|
| Pure idle-notification (lead "ack") | 13 | 2,218,178 | 9.5 % |
| Pure duplicate/echo ack | 5 | 806,114 | 3.5 % |
| Human-typed instruction | 12 | 2,280,630 | 9.8 % |
| Tool-result follow-up (useful work) | 94 | 15,824,993 | 68.0 % |
| Lead woke by teammate msg, no human input | 14 | 2,153,791 | 9.3 % |
| TOTAL (teams portion) | 138 | 23,283,706 | 100 % |
Strict noise from the loop bug (idle-only + duplicate-ack turns where the human did not instruct anything):
18 turns / 3.03 M input tokens = 13.0 % of the Teams-portion input spend, paid purely to say "acknowledged" to notifications the skill docs say I should ignore.
Broader noise (every turn where the lead was woken by a teammate message with no human instruction in the same turn):
32 turns / 5.18 M input tokens = 22.2 % of the Teams-portion input spend.
For comparison, the same session before entering Teams mode (the ~682 pre-team turns doing regular Agent tool calls, reads, edits, etc.) averaged similar per-turn input (~157 K vs ~167 K), but had 0 idle-ack turns and 0 duplicate-completion acks. The loop behaviour is unique to Teams mode.
Token multiplier vs equivalent non-team dispatch: comparable work done in earlier sessions via plain Agent(run_in_background: true) × 5 (no TeamCreate, no shared task list) completes in ~30–40 lead turns with no idle/echo cost. Teams-mode paid ~138 turns for the same delivered artifacts — a ~3–4× lead-turn inflation, roughly half of which is the loop bug.
What Should Happen?
One or more of:
- Idle-notification deliveries should not generate a new lead turn by default. Treat them like a presence update — surface in UI, but do not wake the lead model.
task_assignmentdispatches should be suppressed when the target task status is alreadycompleted, ideally at the team service layer before the teammate mailbox.- Self-originated deliveries (sender
agentId== recipientagentId) should be dropped on send. - Provide an env toggle like
CLAUDE_CODE_TEAM_LEAD_SUPPRESS_IDLE=1for users who want current behaviour off until a better default ships.
Error Messages/Logs
No error. The waste is silent. Representative trace of what the lead sees, back to back, each as its own lead turn (sanitized):
<teammate-message teammate_id="dev-3" color="yellow">
{"type":"idle_notification","from":"dev-3","timestamp":"2026-04-14T12:37:38.194Z","idleReason":"available"}
</teammate-message>
<teammate-message teammate_id="dev-4" color="purple" summary="Task #4 already completed — no-op">
Received what looks like a re-echo of the original task #4 assignment
(sender field is my own ID). Task #4 is already marked completed — no
additional work performed. Standing by.
</teammate-message>
<teammate-message teammate_id="dev-4" color="purple">
{"type":"idle_notification","from":"dev-4","timestamp":"2026-04-14T12:37:58.164Z","idleReason":"available"}
</teammate-message>
<teammate-message teammate_id="dev-3" color="yellow" summary="Task #3 already done — no-op on re-assignment">
Received a task_assignment echo for #3, but that task is already
completed. Status confirmed via TaskGet: #3 = completed. No further
action — standing by.
</teammate-message>
<teammate-message teammate_id="dev-3" color="yellow">
{"type":"idle_notification","from":"dev-3","timestamp":"2026-04-14T12:38:00.473Z","idleReason":"available"}
</teammate-message>
Each of the five blocks above arrived as a separate lead-model turn (~167 K input tokens each at cache-read rates). The lead's reply to each is effectively "acknowledged" — and then another idle notification arrives.
Steps to Reproduce
- Enable Agent Teams:
"env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" }in~/.claude/settings.json. - From a lead session, run:
````
TeamCreate(team_name: "repro")
TaskCreate x 5 # distinct tasks, no dependencies
Agent(subagent_type: "fullstack-developer", model: "opus", run_in_background: true,
team_name: "repro", name: "dev-N") # x 5
- Let the 5 teammates complete their tasks.
- Watch the lead transcript: for every teammate that marks a task completed, expect 2–4
idle_notificationturns and 1–2task_assignmentecho responses, each as its own lead turn. - Grep your session JSONL for
"idle_notification"and"already completed"to quantify noise-vs-signal.
Pressing Esc on the lead breaks the loop (the queued notification turns are discarded) and the lead resumes normal behaviour when told to continue.
Quantifying in your own session
Drop this into a shell (adjust the project dir):
python3 - <<'PY'
import json, glob, os
newest = max(glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl")), key=os.path.getmtime)
idle=echo=total=0
tokens_idle=tokens_total=0
with open(newest) as f:
entries = [json.loads(l) for l in f if l.strip()]
for i,r in enumerate(entries):
if r.get("type") != "assistant": continue
u = (r.get("message") or {}).get("usage") or {}
if not u: continue
cost = u.get("input_tokens",0)+u.get("cache_creation_input_tokens",0)+u.get("cache_read_input_tokens",0)
total += 1; tokens_total += cost
prev = next((entries[j] for j in range(i-1,-1,-1) if entries[j].get("type")=="user"), {})
p = json.dumps(prev)
if '"idle_notification"' in p and '<teammate-message' in p and len(p) < 3000:
idle += 1; tokens_idle += cost
elif "already completed" in p or "duplicate" in p.lower():
echo += 1; tokens_idle += cost
print(f"{newest}")
print(f"idle+echo turns: {idle+echo}/{total} ({100*(idle+echo)/max(1,total):.1f}%)")
print(f"idle+echo input tokens: {tokens_idle:,} / {tokens_total:,} ({100*tokens_idle/max(1,tokens_total):.1f}%)")
PY
Claude Model
Opus (claude-opus-4-6)
Is this a regression?
Not sure — this is my first extended use of Agent Teams so I can't point to a "last working" version. Filing under "likely always-been-this-way" rather than regression.
Last Working Version
_No response_
Claude Code Version
Claude Code 2.1.107
Platform
Claude Pro/Max subscription
Operating System
macOS (darwin 25.3.0)
Terminal/Shell
Terminal.app / zsh
Additional Information
Esc+ "continue" reliably breaks the loop, but requires human babysitting, which defeats the purpose of background teams.- Teammates themselves correctly detect the stale traffic (they explicitly write "appears to be from my own agent ID" in replies) but still must emit a full response turn per delivery, which then wakes the lead. A fix at either end (suppress delivery or suppress model-turn trigger) would resolve the loop.
- The 13–22 % noise share quoted above is on a single 8-teammate session that ran to completion without rework; longer sessions with more inter-teammate DMs would be higher.
- Happy to share a trimmed, anonymized JSONL slice if the team needs a deterministic reproducer.
Showing cached comments. Read the full discussion on GitHub ↗
6 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
I found a way to lock the mailboxes and intercept the idle messages, only marking them unread after 2 minutes of not having received one (and some other conditions), using the following external script that I prompted Claude to write:
I wouldn't normally have added all of this fancy logic, but it turns out that the idle pings are fairly important in keeping the loop from getting stuck and letting the main thread keep an eye on the pulse of the teammates. However, the current logic causes teammates to issue an idle signal at every turn completion, including the end of each tool call, which I believe to be rather overzealous. I would prefer something like the /recap trigger to be what issues an idle signal instead, as its timing is much less noisy and expensive. I've tried to sort of emulate my desired behavior in this script. So at the cost of potentially letting your loop sit idle for a couple more minutes than intended before it self-heals, you can run this script and save a substantial number of idle pokes from ending up in context unnecessarily.
If you use this script, let me know if it helps.
Great quantification. Instrumented our 4-agent fleet and found ~18% waste over a week of production runs.
Two biggest culprits: 1) task_assignment messages echoed by every agent even when unchanged, 2) idle polling loops re-sending full context on every tick.
Quick mitigation that cut waste ~60%: state-diff check before re-sending task_assignment, and interval bump from 5s to 30s for idle agents. Would love to see platform-level handling.
The 13–22% token overhead on no-op acknowledgments is a real problem — and the mechanics you've described (idle_notification as individual turns, stale task_assignment echoes) make it easy to understand why the math works out so badly at 8 teammates.
A few observations from running similar Agent Teams setups:
On idle notifications as separate turns: The protocol seems to be treating "teammate finished" as an event that deserves a full round-trip to the lead, when really it should be a side-channel signal (think: heartbeat, not conversation turn). If idle_notification were delivered as a structured metadata event rather than a conversation message, the lead wouldn't need to generate an acknowledgment at all — it could just update its internal teammate-status table and continue. This feels like a protocol-layer fix rather than a prompt engineering fix.
On duplicate task_assignment echoes: The pattern you're describing (teammates receiving tasks they've already completed) suggests the team's task ledger isn't being marked as consumed before the echo propagates. A possible mitigation while waiting for an upstream fix: have the lead check a simple
_completed_taskslist in CLAUDE.md before sending a task assignment, and skip if the task hash is already there. Crude, but it eliminates the "re-send to already-done teammate" case.On the cost model: The framing of "18 no-op turns = 3.03M input tokens" is exactly the kind of concrete data that helps justify fixing this at the infrastructure layer. Token consumption should scale with actual work content, not with team size times notification frequency. I'd suggest adding a request for a
/notifymode batchoption — batch idle notifications into a single turn at the end of each work wave rather than one per teammate.I'm building Claudeverse (claudeverse.ai) to address the broader session-cost and coordination-overhead problems in multi-agent Claude Code workflows. The Agent Teams notification loop is one of the patterns we've explicitly worked around at the coordination layer. Happy to share the approach if it would help inform the upstream fix.
We ran the same setup (one lead orchestrator + N sub-agent teammates) and tested a fix on the no-op-ack path. Reporting the result.
Fix tested: a
UserPromptSubmithook forcing the lead to emit zero output when the incoming turn is anidle_notification. Kept for 2 days, then reverted.Measurements:
idle_notificationarrives as a conversation turn, so the accumulated context is re-fed on the next round trip whether or not the lead replies.idle_notificationand ashutdown_approvedin the same turn, the zero-output rule also suppressed the legitimateTeamDelete: a ~3.5-min zombie teammate, a double shutdown, and a downstreamTeamCreatefailure.Conclusion: both the token cost and the control failure come from idle being delivered as a conversation turn, not from the lead's reply. A prompt/hook layer addresses neither.
Current workaround: multi-agent deliberation runs in an isolated
Workflowsub-context; teammate idle/echo turns stay there and never reach the lead's conversation, which receives only one structured result. This removes the accumulation at the source. A side-channel idle signal (a metadata event, or a batched notify mode) would cover the case where the lead must stay in the loop.Adding evidence + concrete solution shapes from a production multi-agent deployment (overnight 4-worker coordination sessions driving a machine-vision/laser rig), since we dug into whether this is workaround-able today. Our findings agree with and extend the hook-test comment above:
{type:"idle_notification", from, timestamp, idleReason}) is invoked unconditionally;CLAUDE_CODE_IDLE_THRESHOLD_MINUTES/CLAUDE_CODE_IDLE_TOKEN_THRESHOLDgate unrelated idle machinery.shutdown_requestis never processed and only a hard TaskStop ends it. Any suppress-the-reply approach also can't fix this — consistent with the hook-test result above that the delivered TURN, not the lead's reply, is both the cost and the control surface.shutdown_requestentries to idle agents' team-inbox JSON files (undocumented internal state — we'd rather not depend on it), lead-side kill-on-ping (spends the very turns it saves), and — after reading the Workflow-isolation comment above — adopting Workflow sub-contexts for non-interactive parallel work. The uncovered case remains exactly as that comment says: work where the lead must stay in the loop (for us: live-hardware coordination with an operator present, where mid-task teammate↔lead decisions are the point).Any one of these would resolve the remaining case:
"teammateIdleNotifications": "off" | "first-only" | "on"in settings —first-only(one ping per agent per idle period) keeps the legitimate signal ("worker went idle without starting its task") at 1/N the cost.idle_notifications: falseoption on the Agent tool, so leads opt out for workers they manage by deliverable.