[Bug]: idle nested subagent chains stay in the shell snapshot with omitIdleSubagents#29128

opened10d agobyFfireboltdude1357

Area

apps/server

Steps to reproduce

  1. Use subagents that start their own subagents until a project has several nested chains, then let them all finish.
  2. Connect a client that asks for omitIdleSubagents (web, desktop and mobile all do), over subscribeShell or the HTTP shell snapshot.
  3. Count the subagent rows in the snapshot.

Expected behavior

Idle subagents that aren't above a live subagent are left out, as idle leaf subagents already are, and load on demand through getThreadFamily.

Actual behavior

omitIdleSubagentShells in apps/server/src/orchestration-v2/ShellStream.ts keeps every idle subagent that is the parent of another subagent (facts.subagentParentIds), so only the bottom row of each idle chain is dropped. On one real database, the cold-connect shell has 331 rows. Leaving out idle chains with no live subagent below them leaves 54. Every connect and reconnect pays for the extra rows, which shows most on mobile over a remote connection.

The rows are there so the badge and working walks over shell rows never reach a missing parent. Dropping them needs the client to cope with a gap: a chain that wakes after connect arrives as one row whose parent the client doesn't have, and the server can't send the parent rows with that delta, because the shell reducer skips events at or below the last applied sequence.

Impact

Minor bug or occasional failure

Version or commit

main @ 30654b3e91

Environment

T3 server on Linux (WSL2), web and mobile clients, remote connection over Tailscale.

Synced from GitHub

Another data point, at larger scale: desktop and server 0.1.6, macOS 27.0.1, Apple silicon.

Rows. The database had 65 active top-level threads and 20,036 unarchived subagent threads, almost all Claude native subagents. 14,870 of them had another subagent as their parent. None had a running run; 3 had a queued one. 1,833 subagents were the parent of another subagent, all but one idle, so omitIdleSubagentShells kept them. A cold-connect shell was therefore about 1,900 rows. Dropping idle chains with nothing live below them, as #29129 does, would leave about 70.

Freeze. Opening the largest thread (674 direct children) in a fresh headless web client against the local 0.1.6 server gave one 4.6 s task. 4.1 s of it was inside the environment-quiet-thread-shell-list read function, the #25823 path. After I deleted the idle subagent threads with thread.delete, the same steps gave no task over 200 ms. I did not keep the before profile, only its numbers. The delete removed every idle row at once, so it doesn't show how much of the freeze is the row count and how much is the per-rebuild cost that #25853 targets. It also removed that thread's own children, which opening it would still load under #29129, so it overstates what #29129 alone removes.

New subagent threads arrived at about 1,200 a day (700 to 2,200 over the previous 10 days). A client that stays connected also keeps the ones created after it connected (#29458).

Given the freeze, which the row count feeds, an impact above "Minor" seems warranted, for the author or triage to decide.