Finished Claude delegate_task sessions keep their claude process and MCP servers for 30 minutes, so delegate-heavy runs exhaust RAM#26413

opened13d agobyPPjieter

What happened

what processes are hogging memory, and which can we close?

A 16 GB Windows machine was down to 0.9-2.1 GB free RAM. The memory was held by 18 claude.exe processes under the T3 Code desktop server, each with its own set of MCP servers (about 24 child processes per session: python, uv, uvx, node, conhost). 12 of the 18 belonged to delegate_task child threads whose single run had already completed. One exited on its own; killing the other 11 trees freed about 5 GB of RAM at once (0.9 GB free to 6.1 GB free); settling the threads afterwards is what the user had meant to do.

Diagnosis

A Claude provider session runs one claude process per thread (supportsMultipleProviderThreadsPerSession: false). When a turn ends, ProviderSessionManager keeps the session until it has been idle for idleTimeoutMs, which is DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000 (apps/server/src/orchestration-v2/ProviderSessionManager.ts:56, applied at :387, timer in scheduleIdleReleaseInternal :1167 and releaseIfStillIdle :1074). No caller outside tests passes idleTimeoutMs, so it is not configurable, and there is no cap on how many idle sessions may stay live.

The only early release paths are detach (settle, archive, delete, workspace or selection change) and instance close. Settling does release the process: thread.settle queues provider-session.detach for every live session (Orchestrator.ts:3848-3862), and detaching the last thread calls releaseEntry(..., "manual_shutdown") (ProviderSessionManager.ts:2398-2406). But nothing settles a delegated task when it completes. Auto-settlement only works in whole days (sidebarAutoSettleAfterDays) or on PR merge (ThreadSettlementService.ts:235-241).

Delegated tasks are almost never resumed, so the warm 30 minutes buys nothing for them. On this machine today, 74 of 78 delegated-task threads ran exactly once, with a median run of 1.7 minutes (p90 7.0 minutes). Each one then held a claude process plus its MCP servers for 30 more minutes. A workflow that starts a delegate every minute or two (implement, review, re-review, two-seat review per task, across three parent threads) keeps 15-28 sessions live, nearly all idle.

Per-session cost here was 490-630 MB private for claude.exe and 1.0-1.5 GB private for the whole tree with the user's 5 stdio MCP servers. The MCP set is user configuration, but T3 decides how long the process lives, and that multiplies whatever it costs.

Same pattern as #16917 for Codex, from the Claude side: a delegated child "runs one turn, completes, and is never settled". Codex got a per-thread idle unload on its shared app-server; Claude has one process per thread, so the whole process stays up for 30 minutes.

Steps to reproduce

  1. Configure a few stdio MCP servers for Claude Code (any that start a Python or Node process).
  2. In one Claude thread, run a workflow that calls delegate_task (Claude provider) about once a minute for 20 minutes, each task a short review that finishes in 1-3 minutes. Do not settle the children.
  3. Count claude processes under the T3 server and their descendants. It grows to roughly one per delegate started in the last 30 minutes, and each process exits 30 minutes after its task finished.
  4. Settle one finished child: its claude process and MCP servers exit within a second.

Version

0.1.3 (desktop app; /.well-known/t3/environment reports serverVersion: 0.1.3). The last t3 triage CLI on this machine was 0.1.2.

Environment

Windows 10 Enterprise 19045 x64, 16 GB RAM, T3 Code (Beta) desktop with local server, Node v26.12.1, Claude Code 2.1.311, 5 stdio MCP servers configured for Claude (3 via uvx, 2 via node).

Evidence

shell
1# statev2.sqlite, read-only. Claude provider sessions created 2026-10-27 that reached "stopped" (real end times):
2stopped claude sessions today: 61 of which delegated-task threads: 53
3release time minus thread's last run completion (min): median=30.0 p10=30.0 p90=30.0
4 buckets: {'<5': 1, '5-25': 3, '25-35': 54, '35-60': 1, '>60': 2}
5
6# live Claude sessions vs. sessions with a run in progress
7time live busy idle idle_delegates
814:40Z 12 1 11 7
915:00Z 20 2 18 14
1015:20Z 27 0 27 23
1115:40Z 17 0 17 13
12peak 15:15Z: live=28 busy=2 idle=26 idle_delegates=22
13
14# claude delegate runs today: n=71 median=1.7 min p90=7.0 min; delegate threads with >1 run: 4 of 78
15
16# process snapshot 19:29Z (18 claude.exe trees, private MB per tree)
17claude.exe 486-632 MB each; tree incl. MCP servers 1022-1477 MB each; ~24 descendants each
18OS: 15.7 GB total, 2.1 GB free (0.9 GB free a minute later)
19
20# settle releases the process: detach effects queued by thread.settle, all succeeded
21orchestration_v2_effect_outbox provider-session.detach "Thread settled." status=succeeded (11 of 11, each < 1 s)

#16917 (merged) fixed the Codex half of this: idle Codex threads on the shared app-server are unloaded after idleTimeoutMs. Claude sessions are separate processes and still live 30 min each. #21354 is Codex subagent children never unsubscribed. #24048 and #26004 are memory pressure from uncapped agent children (macOS, Linux). #18485 is a Windows report where the memory sat in MCP helpers under the Codex app-server. #15173 shows the opposite risk: a parent released while it waited on a delegate, so any fix should apply to finished children, not to parents waiting on them. None of these covers finished Claude delegates.

Fix applied or workaround

Killed 11 idle claude.exe trees of completed delegate threads (taskkill /T /F), then settled all 12 threads. Free RAM went from 0.9 GB to 6.1 GB. Settling a finished delegate on its own releases its process. As a workaround I'm trying a one-line rule in my global CLAUDE.md that tells the parent agent to settle each delegate_task thread once it has read the result. It depends on the agent following the rule, and it does nothing for delegates that are still waiting for the parent to read them.

Possible fixes, in rough order of payoff: release a delegated-task child's session as soon as its run completes and its result has been delivered to the parent (or use a much shorter idle timeout for delegated-task threads); expose idleTimeoutMs as a setting; cap idle live sessions and evict the oldest idle one first.

Filed by

Claude Code (claude-opus-5-5) via the t3 triage playbook, run from inside a T3 Code thread.

Synced from GitHub

#25624 is an open PR for the second fix. It makes the idle timeout a setting, from 1 to 1440 minutes or Never.

That setting applies to every thread in the environment. Claude threads that message each other with SendMessage have to stay alive while idle (#9586).

Synced from GitHub

The same cost on a second machine, and a fix ready if its direction is the one you want.

Cost here (Windows 11 PC, T3 Code with heavy delegate_task use, 11-12 live Claude sessions sampled twice 17 minutes apart): each session holds about 0.86-0.94 GB (claude.exe 650-750 MB plus about 180 MB of MCP children), and 9 of 11 used under 0.5 s of CPU per 30 s. So 6 finished delegates hold about 5.2-5.6 GB for up to 30 minutes.

Fix: Pyseph:fix/release-finished-delegate-sessions, one commit on main, about 80 source lines plus tests. When a delegated child's run completes and its result is recorded for the parent, the Orchestrator queues an outbox effect that releases the child's session, but only if nothing can still resume it:

  • the session belongs to this thread alone (shared Codex-style processes keep their per-thread unload);
  • the thread is a subagent child with no active or queued run, no running subagent, no pending completion delivery and no background task holding completion;
  • no turn is busy and the adapter reports no pending background work (buffered wake messages, running native subagents);
  • nothing happened since the check began (the idle timer's own onlyIfIdleGeneration guard).

Otherwise, and for failed, cancelled or interrupted runs, today's 30 minute timer still applies. The release goes through the existing releaseEntry path, and a later message to the child opens a new session the way it does after any idle release. Ordinary threads keep the 30 minute default.

With the repo's fake provider, where each session spawns a real idle node process standing in for claude.exe, 6 delegated sessions with completed runs, 10 runs per variant alternating: on main all 6 stay alive until the timer (6 alive at 29:59, 0 at 30:00 on the virtual clock); on the branch all 6 exit a median 20.5 ms (11-30 ms) after the release call. Not measured: real Claude sessions, a real delegate_task run end to end, and the outbox delay before the release call.

Trade-offs:

  • A follow-up to the same child within 30 minutes pays a session start (spawn, MCP registration, transcript resume) instead of reusing the warm process.
  • A released child stops being a live SendMessage endpoint at once instead of after 30 minutes (the #9586 concern).
  • It adds an outbox effect type to the persisted schema.
  • It composes with #25624: that sets one timeout for every thread, this only ends finished delegates early.

Question for maintainers: do you want a completed delegate's session released as soon as its result is delivered? The issue's alternatives are a shorter idle timeout for delegate threads only, or relying on #25624. If you confirm a direction, I'll open a focused PR for it, this branch or adapted.

Written by Claude Opus 5.5 in T3 Code (Claude Code harness).

Synced from GitHub

+1, we're seeing this on a 16 GB Windows machine running T3 nightly with heavy delegate_task use. On 9 Nov 2026 we had 37 claude.exe processes, all children of one t3.exe, using 16.43 GiB of private bytes. Available RAM fell to 1.18 GiB of 15.41 GiB. Any of options (a)–(c) would help, and we'd especially like release-on-completion. Your point that "there is no cap on how many idle sessions may stay live" is the part none of the three options addresses for threads that never sit idle for 30 minutes, so we've proposed an optional concurrent-session cap here as a complement: https://github.com/pingdotgg/t3code/discussions/33413