The inbox poller added in #291 wakes the Lead on teammate inbox messages, but a
completed background task still strands its result: nothing wakes the Lead for it,
and collect_background_results() runs only inside the tool-use branch, so a
plain-text turn never picks it up. There is also no signal once every teammate has
finished.
Extend the same poller instead of adding a second path. has_pending_background()
reports, without consuming, whether any background task has completed. The poller
now wakes on unread inbox messages or a completed background task, and the wake
handler drains both the inbox and collect_background_results() before running the
turn, so background results become new turns without user input, the same way
teammate results already do. When the last teammate finishes and its output has
been drained, a single "[all teammates done]" marker is printed.
After the Lead spawns teammate agents, if the current round terminates with
plain text (stop_reason != tool_use), agent_loop returns and the REPL falls
back to a blocking input(). Teammates run as daemon threads and finish later,
so the results they send to the Lead's inbox stay unconsumed until the user
submits their next input.
The cause is that result delivery is bound to the turn loop, whose lifetime
ends when the model stops calling tools, while a teammate completes on its own
clock. Blocking a teammate is harmless because it runs as a background thread;
the problem is specific to the Lead, which owns the user prompt.
The fix follows real Claude Code (useInboxPoller, described in the appendix):
decouple input() from turn execution. MessageBus.peek(agent) reports whether
the inbox has unread messages without consuming them. In __main__, input()
moves to a dedicated daemon thread and an inbox poller thread peeks the Lead's
inbox every second; both push to one shared event queue. The main thread runs
one turn per event, woken by either user input or an incoming teammate message,
so teammate results become new turns without waiting for the user. Repeated
inbox wakeups are idempotent: an empty read is skipped when a prior read_inbox
already drained the messages. Only the Lead's main loop changes; the teammates'
idle loops are untouched.