Agent teams: idle teammates are never evicted — mailbox poll loop ran ~143k times (~20h), wedging the session

Status Open
Reported on v2.1.245
Maintainer reply None cached
Activity 0 comments · opened Aug 25, 2026

Summary

With CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1, named teammates that have
finished their work are never evicted. They idle-poll their mailbox every
500ms indefinitely, which keeps the session "thinking" forever.

In my case five teammates polled ~143,000 times each — roughly 20 hours
— and every single read returned 0 unread of 0 total. The session only
stopped because I killed the process.

There is no API traffic during this window (it's local file I/O), so it
doesn't burn tokens. The cost is a permanently wedged session.

Environment

  • Claude Code 2.1.245 (installed via the native installer)
  • macOS Darwin 25.5.0, arm64
  • "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" in ~/.claude/settings.json

Evidence

Five named teammates in team session-c823f2f2. Final poll counters before
the process was killed:

costguard-survey   #143182
savings-hunt       #142944
attribution        #142860
routing-metric     #142823
current-merge-2    #135631

Measured poll interval from ~/Library/Logs/claude-code/debug.log:
median 0.502s (n=280, min 0.500, max 0.512). 143,182 polls at that
interval is ~19.9 hours.

Interleaved mailbox reads, for the entire window:

2026-08-24T22:11:30.261Z [DEBUG] [TeammateMailbox] readUnreadMessages: 0 unread of 0 total
2026-08-24T22:11:31.037Z [DEBUG] [TeammateMailbox] readUnreadMessages: 0 unread of 0 total
2026-08-24T22:11:31.262Z [DEBUG] [TeammateMailbox] readUnreadMessages: 0 unread of 0 total

All five stop at the same instant, which is the kill, not five completions:

2026-08-24T22:13:50.586Z [DEBUG] [inProcessRunner] attribution poll #142860: checking mailbox
2026-08-24T22:13:50.586Z [DEBUG] [inProcessRunner] routing-metric poll #142823: checking mailbox
2026-08-24T22:13:50.960Z [DEBUG] [inProcessRunner] costguard-survey poll #143182: checking mailbox

Reproduction

  1. Set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1.
  2. Spawn a named teammate via Agent with a name.
  3. Let it finish its task and go idle. Do not send it a shutdown request.
  4. Watch the debug log — the poll counter climbs forever.
grep -o '\[inProcessRunner\] [a-z0-9-]* poll #[0-9]*' \
  ~/Library/Logs/claude-code/debug.log | tail -5

Six-figure poll numbers mean the session has been spinning for hours.

Where I think it comes from

Caveat up front: I don't have source access, so this is read off strings
against the shipped bundle. Symbol names are minified and I could not
resolve one of them (noted below). Treat the mechanism as a hypothesis
that fits the logs, not as a confirmed root cause.

1. The poll loop has a vestigial idle deadline.

let d = Date.now(), p = !1, f = 0;
while (!t.signal.aborted) {
  if (f > 0) await en(500);
  f++;
  ...
  if (l || h?.awaitingPlanApproval || ... ) d = Date.now();
  ...
}

d is assigned on activity and, as far as I can tell, never read anywhere
in the loop body. It looks like an idle timeout that was intended but never
wired up.

2. The eviction path exists but is unreachable while a predicate holds.

An idle teammate does get a deadline — 30s:

sE = 30000
// on going idle at turn end:
evictAfter = Date.now() + sE

The reaper runs on a 1000ms interval, and the branch order is:

if (yl(xe.status))                            -> evictTerminal
else if (Ze && xe.isIdle && xe.evictAfter>0)  -> renew list
else if (xe.evictAfter>0 && xe.evictAfter<=ee)-> evict list
...
for (let ye of _e) { if (PSe(je)) le[ye] = {...je, evictAfter: ee + bSe} }

The renew branch is tested before the evict branch. So while Ze
is truthy, an idle teammate has its deadline pushed forward once per second
and the 30s timer never matures — which is exactly the observed behavior.

Ze comes from a call on the task list. I could not determine what it
actually tests: the minified name collides with unrelated symbols elsewhere
in the bundle, and the strings dump loses scope. If it's something like
"the team still has any live task," then any long-running teammate pins all
of its idle siblings alive, which would explain why all five stayed up.

Suggested fixes

  • Check the evict branch before the renew branch, or exclude idle teammates

from renewal.

  • Wire up (or delete) the lastActivity timestamp in the poll loop, and

give the loop an absolute idle cap so it can't outlive its deadline
regardless of the reaper.

  • Consider backing the poll off after N idle iterations — a 500ms fixed

interval for 143k consecutive empty reads is a lot of wasted wakeups.

Workarounds for anyone hitting this

  • Prefer one-shot Agent calls over named teammates — they exit cleanly.
  • If you do use a teammate, explicitly end it when done: TaskStop with the

teammate name, or SendMessage with {"type":"shutdown_request"}. There
is a real shutdown path that clears evictAfter.

  • Or unset CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS.

View original on GitHub ↗