Agent teams: idle teammates are never evicted — mailbox poll loop ran ~143k times (~20h), wedging the session
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
- Set
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1. - Spawn a named teammate via
Agentwith aname. - Let it finish its task and go idle. Do not send it a shutdown request.
- 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
lastActivitytimestamp 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
Agentcalls over named teammates — they exit cleanly. - If you do use a teammate, explicitly end it when done:
TaskStopwith 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.