Passive app-server subscriptions permanently pin idle thread heaps

Resolved 💬 0 comments Opened Jul 28, 2026 by Tsury Closed Jul 28, 2026

What version of Codex CLI is running?

Observed on 0.146.0-alpha.10.1. The relevant behavior remains present in rust-v0.146.0-alpha.14.

What platform is your computer?

Linux x86_64, long-lived app-server process.

What issue are you seeing?

Passive app-server thread/subscribe connections permanently prevent idle thread unloading. A client that observes many threads for status and event delivery therefore becomes a residency owner for every observed thread, retaining each loaded conversation history indefinitely.

Current app-server state tracks one undifferentiated connection_ids set per thread. thread/subscribe enters that same set, and UnloadingState watches whether the set has any connections. As long as an observer remains connected, the 30-minute idle unload timer can never begin.

In one long-lived process:

  • 18 observed threads remained loaded.
  • Canonical rollout JSONL totaled about 1.54 GiB.
  • App-server private anonymous heap reached about 12.8 GiB after 23.5 hours.
  • File-backed memory was only about 151 MiB; no child tool process or swap use remained.
  • Crossing a 12 GiB cgroup memory.high produced about 87% full memory PSI and roughly 66,000 high-throttle events per 10 seconds.
  • New app-server clients and metadata requests timed out while some established turns continued slowly.
  • Restarting only app-server immediately reclaimed the memory.

The exact split between live decoded history, other retained conversation state, and allocator fragmentation requires heap profiling. The residency bug is deterministic: passive subscriptions keep has_subscribers true, so idle histories are never eligible for unload.

What steps can reproduce the bug?

  1. Start one app-server process.
  2. Create or resume multiple large threads.
  3. Keep one client connected and call thread/subscribe for every thread so it can observe status and events.
  4. Leave those threads idle while keeping the observer connection alive.
  5. Observe that none transition through normal idle unload, even after THREAD_UNLOADING_DELAY.
  6. Repeat with more large threads. Process RSS grows with retained histories and does not return while the passive subscriptions remain.

What is the expected behavior?

Event observation and thread residency should be separate:

  • A passive thread/subscribe observer should continue receiving thread events and server requests.
  • Passive observation alone should not prevent an idle thread from unloading.
  • Starting, resuming, or actively operating a thread may promote that connection to a residency owner.
  • An idle thread with no residency owners should unload after the normal grace period without disconnecting the observer from future event delivery.

After successful unload, all conversation references should be released. On glibc Linux, a coalesced malloc_trim(0) after complete removal can return allocator-retained free pages, but the primary fix is making passive observation non-resident.

Additional information

This is not the same request as:

  • #21889, which proposes an explicit operator-driven thread/unload RPC.
  • #31579, which concerns stale poisoned in-memory history.
  • #26015, which reports broader Windows Desktop memory growth.

An explicit unload RPC does not replace passive observer semantics: a status/event bridge must keep observing threads continuously without manually racing activity or losing event delivery.

View original on GitHub ↗