Repository navigation
Description
Summary
The app-server broker shares one upstream connection across plugin tasks.
Each task starts or resumes a thread, which subscribes that upstream connection.
The downstream task socket closes after completion.
The broker clears routing state but never sends thread/unsubscribe.
Completed threads and their MCP runtimes remain loaded until unsubscribe or app-server shutdown.
Observed impact
- Fresh tasks accumulate MCP runtimes inside one broker session.
- One app-server retained 11 Basic Memory runtimes.
- Other app-servers retained seven, five, and five runtimes.
- Basic Memory only made the generic broker defect visible.
- The proposed fix contains no Basic Memory dependency.
Reproduction
- Configure any stdio MCP server in Codex.
- Start Claude Code with the Codex plugin.
- Run several fresh Codex tasks in one workspace.
- Let every task finish normally.
- Inspect the app-server process tree.
Observed result
- Each fresh task creates another thread and MCP runtime.
- The downstream task client disconnects.
- The upstream thread subscription remains active.
- Completed tasks therefore accumulate MCP processes.
Expected behavior
The broker should track thread ownership for each downstream socket.
It should send thread/unsubscribe after the final owner disconnects.
Cause
thread/start subscribes the shared upstream connection to thread events.
The broker does not associate that subscription with its downstream owner.
Socket closure therefore removes routing state without removing the subscription.
The final unsubscribe starts Codex's documented 30-minute inactivity period.
Codex unloads the thread and MCP runtime after that period.
Proposed fix
- Track subscribed threads for each downstream socket.
- Preserve subscriptions while another downstream owner remains.
- Cover started, resumed, forked, review, and automatically created child threads.
- Unsubscribe after normal or abrupt final-owner disconnection.
- Log upstream unsubscribe failures.
- Add broker ownership regression tests.
Real-process verification
Tested with Codex CLI 0.150.1 and Basic Memory.
| Case | Probe after client closure | Meaning | MCP process result |
|---|---|---|---|
| Control broker | unsubscribed |
The broker retained the subscription | Exited approximately 30 minutes after the probe unsubscribed |
| Patched broker | notSubscribed |
The broker had already unsubscribed | Exited approximately 30 minutes after client closure |
Both cases started a real Basic Memory wrapper and Python server.
Every isolated test descendant exited during cleanup.
Related work
- app-server brokers never self-terminate → orphaned process/RAM leak (34 chains, 272 procs, ~2.2GB) #543 tracks retained broker and app-server process trees.
- Reap leaked broker sessions across working directories #660 proposes broader broker supervision and cleanup.
- fix(broker): shut down the app-server broker after an idle timeout #680 proposes broker idle shutdown.
Those changes bound session lifetime.
This issue addresses per-task thread retention inside a running broker.
Activity
dot-agi commentedon Sep 11, 2026
Corroborating this at much larger scale, plus one correction on the unload delay.
Observed (macOS 26.6, plugin 1.0.6, codex-cli 0.154.0): a single broker for one worktree had been alive for 2d 17h — its owning sessions were SIGKILLed by macOS jetsam, so
SessionEndnever ran. Over ~94 codex sessions (rescues + Stop-hook review gates) it accumulated 1,218 processes under onecodex app-server:i.e. one full
[mcp_servers.*]set per thread, never released. A jetsam snapshot attributed 60.4 GB of footprint to the 957nodeprocesses and 12.3 GB to 80claudeprocesses; swap was 63.9 / 64.5 GB, and the ~1,000 V8 address spaces pushed kernel page tables (VM_KERN_MEMORY_PTE) to 11.6 GB of wired memory. SIGTERM to the broker released everything (1,969 → 632 processes, swap 62.7 → 15.8 GB).The task path and the review path leak the same way — a review holds two threads (the
thread/startone plusreviewThreadIdfromreview/start).Correction on the 30-minute figure: in codex
rust-v0.154.0,thread_unload_delay_secsdefaults to 60 s (codex-rs/core/src/config/mod.rs:cfg.thread_unload_delay_secs.unwrap_or(60); the config schema says "Seconds a thread must have no subscribers and no activity before app-server unloads it. Defaults to 60"). So a correctthread/unsubscribereleases the MCP runtime about a minute after the client disconnects.thread/archiveis not a substitute for the ephemeral threads used by reviews and non-persisted tasks: it reads the thread from the persisted store and errors forephemeral: truethreads.Verified #707 fixes it (details on the PR): A/B against the real
codex app-server0.154.0 —thread/startspawns the 14-process MCP set; after the downstream client disconnects, the unpatched broker still holds all 14 after 150 s, while the patched broker releases them at ~61 s.Harden the transport and broker substrate (upstream audit)
youngjin39 commentedon Oct 8, 2026
Still reproduces on plugin 1.0.6 + codex-cli 0.159.2 (macOS). Besides memory, a project-scoped stdio MCP server that keeps a SQLite database open was left running 113x under one broker, which tripped that project's open-handle safety gate.
Workaround until #707 ships, without patching the plugin: the broker forwards any JSON-RPC request from a socket client to its app-server (and answers "Shared Codex broker is busy" while a task runs). An external script can connect to the broker's unix socket (
--endpoint unix:...in its argv),initialize,thread/loaded/list,thread/readeach id, and sendthread/unsubscribefor threads whosestatus.typeisidleand whoserecencyAt/updatedAtis old; the app-server unloads them ~60 s later and their MCP servers exit. For a broker whose workspace has no Claude session left (#791, #108),broker/shutdownstops the broker, its app-server and every MCP server cleanly. Verified on a self-started 1.0.6 broker: 6 MCP children -> 0, no orphans. Caveat: an unsubscribed ephemeral thread cannot be resumed.omarha2023 commentedon Oct 8, 2026
Another occurrence, with a measurement that supports the fix in #707.
Environment: plugin
openai-codex/codex1.0.6,codex-cli0.161.0, macOS (arm64).Observed: every
codex-companion.mjs taskjob opens a thread on the shared broker'scodex app-server. Each thread spawns its own set of the user-configured MCP servers, and the set stays alive after the job ends. A failed turn adds a set too. Three minimal jobs added three sets, and none of them exited.Controlled check: after the jobs finished, I sent the RPC through the broker endpoint myself (
CodexAppServerClient.connect+request):thread/unsubscribe {threadId}returned{"status":"unsubscribed"}. That thread's MCP set exited 30 to 60 s later.thread/archive {threadId}returned{}. That thread's MCP set exited within 30 s.thread/loaded/listreturns the threads still holding sets, so a reaper can find them. Unsubscribing after each job, as #707 does, is enough, and it keeps the thread resumable.viktor7ornst commentedon Oct 20, 2026
Another data point, this time severe enough to crash the Claude desktop app — please consider merging #707.
Environment: plugin
openai-codex/codex1.0.2 (the reports above show the same on 1.0.6),codex-cli0.163.0, Claude Code 2.1.303–2.1.304 (desktop app + CLI), macOS on Apple silicon (10 cores). Many Claude Code sessions run in parallel and use the plugin for reviews/rescues throughout the day.Observed after ~1.5 days:
app-server-broker.mjs), all idle (0 % CPU), each holding acodex app-server.node; per broker the same set kept repeating:npm exec xcodebuildmcp@latest mcp,npm exec @playwright/mcp@latest, Codex Computer Use client, ChatGPTnode_repl, a meetings MCP and other user-configured MCP servers.Mitigation: terminating the 17 brokers older than 3 h (SIGTERM to the broker, then its remaining tree) ended 1,863 processes; nothing else was affected and the plugin started fresh brokers on demand. Until #707 ships we run an hourly local reaper that terminates brokers older than 3 h whose process tree used no CPU in the last hour.
This matches the root cause described here: completed threads are never unsubscribed, so their MCP runtimes stay loaded for the lifetime of the app-server. With several busy sessions the leak grows by roughly ten processes per review and takes the machine down within a day or two, so a release with #707 (or an idle timeout per #543/#108) would be very welcome.
QQism commentedon Nov 5, 2026
Confirming on plugin 1.0.6 with Codex CLI 0.166. One long-lived Claude Code session's broker had loaded 19 task threads over about two days, each with its own set of MCP server processes (about 10 GB RSS in total).
A client-side workaround works: calling
thread/unsubscribefor each thread a task started (the task thread, plus the review thread and any sub-agent threads collected in the turn state) at the end ofrunAppServerTurnandrunAppServerReviewinscripts/lib/codex.mjs. In a repro through the broker, two tasks left 20 MCP processes behind without it. With it, all of them were released once the unload delay passed. Settingthread_unload_delay_secsin~/.codex/config.tomlshortens the default 30-minute delay.Tracking ownership per downstream socket in the broker, as proposed above, would also cover tasks that crash before they reach the end.
gabssolutions2022-pixel commentedon Nov 5, 2026
Another data point on Windows that matches this issue, with the stop review gate as the trigger.
Setup
codex1.0.6 (latest release),codex-cli0.166.0, Windows 11.stopReviewGate: truein one workspace only. Three stdio MCP servers from~/.codex/config.tomlstart per thread: Context7, Playwright, and MarkItDown. That is four processes per thread, because MarkItDown runs as two.Observed
codex app-serverheld 144 MCP server processes after about 17 hours in one Claude Code session.state.jsonfor that workspace lists 36 completedtaskjobs in that window, all in statuscompleted.createdAtmatches, to the minute, the start time of one new set of MCP servers under the app-server.Impact
.pyd, which blocksuv syncof the MCP environments until the broker exits.Workaround
broker/shutdownto the broker endpoint, the same pathSessionEnduses, releases all of them. The next task starts a fresh broker.Unsubscribing completed task threads, as proposed in #707, would fix this case.
rugobal commentedon Nov 12, 2026
Additional Linux confirmation on plugin 1.0.6, with measured cleanup results.
Environment
db52e28f4d9ded852ab3942cea316258ae4ef346.Before cleanup
app-server-broker.mjs servetrees, containing 1,357 processes.idle, onesystemError. All matched saved terminal job records: 198 completed jobs and one failed job across those workspaces.node_replprocesses, plus wrappers and sandbox/code-mode helpers.Cleanup and results
After checking saved job receipts, idle/error thread status, and absence of downstream clients, we backed up the completed job records and invoked the existing
broker/shutdownRPC for each eligible broker. All 47 shut down, and all 1,357 original processes exited; no surviving descendants were found. Other running Codex/Claude sessions and their helpers were left running.MemTotal - MemAvailable)That recovered 33.28 GiB of available RAM, with 18.16 GiB less logical swap. Swap was zram-backed; the logical swap figures should not be added to resident RAM as physical usage.
The installed code matches the lifecycle problem described here: downstream client closure clears broker socket ownership, while the shared upstream app-server remains alive without a matching
thread/unsubscribe. Review execution also uses ephemeral threads, so persisted job results were checked before disposing of those runtimes.This provides a Linux data point for the subscription cleanup proposed in #707, alongside the broker-lifetime problems tracked in #543. A released fix covering completed/failed task and review threads, including abrupt client disconnects, would prevent this accumulation.