app-server websocket disconnect leaves orphaned MCP-backed threads loaded
Summary
When the last app-server websocket connection drops without sending thread/unsubscribe, the loaded thread remains in memory and its MCP child process stays alive.
The root cause appears to be that connection_closed removes connection bookkeeping but does not unload threads that now have zero subscribers. thread/unsubscribe already has the correct unload path, but disconnects do not go through it.
Impact
In the desktop app this lets MCP-backed threads accumulate under the long-lived app-server process. Over time that leaves large numbers of retained biz-cosense-mcp-server, playwright-mcp, and wrapper processes alive after their owning connections are gone.
Reproduction
A deterministic regression test can reproduce this with a real stdio MCP child process:
- Start app-server in websocket mode with a stdio MCP server configured.
- Connect websocket client A and initialize it.
- Start a thread so the MCP stdio child process is launched.
- Verify the thread appears in
thread/loaded/listand the MCP child process is alive. - Close websocket client A without sending
thread/unsubscribe. - Reconnect as websocket client B and inspect
thread/loaded/list.
Expected
- The last disconnect should unload the orphaned thread.
- The MCP stdio child process should exit.
- A subsequent
thread/loaded/listshould be empty.
Actual
- The thread remains loaded after the last websocket disconnect.
- The MCP stdio child process stays alive.
- The new regression test times out waiting for the child process to exit.
Evidence
Before the fix, this targeted test fails:
mise exec rust@1.93.0 -- cargo test -p codex-app-server websocket_disconnect_unloads_thread_and_its_mcp_process -- --nocapture
Failure mode:
wait_for_process_exit(&pid)times out after websocket close.- Reconnecting still shows the thread in
thread/loaded/list.
Acceptance Criteria
connection_closedunloads threads whose last subscriber disappeared.- The disconnect path reuses the same thread teardown logic already used by
thread/unsubscribe. - A regression test covers websocket disconnect cleanup with a real stdio MCP child process and passes.
- Existing
thread_unsubscribetests continue to pass.
4 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Tested fix is available here:
0f73152d17b4a42f28a884f0a35eea123a16256cI could not open a PR because
openai/codexis configured withpull_request_creation_policy: collaborators_only.Verified locally:
mise exec rust@1.93.0 -- cargo test -p codex-app-server websocket_disconnect_unloads_thread_and_its_mcp_process -- --nocapturemise exec rust@1.93.0 -- cargo test -p codex-app-server removing_last_connection_returns_orphaned_thread -- --nocapturemise exec rust@1.93.0 -- cargo test -p codex-app-server thread_unsubscribe -- --nocaptureI checked the duplicate suggestions.
Closest related issues:
stale MCP helper process trees)Ctrl+ZI think this issue is still worth keeping open because it narrows one concrete app-server cause with a deterministic repro and tested fix:
thread/unsubscribe,connection_closeddrops connection bookkeeping but does not unload the thread that now has zero subscribersSo my read is:
#14950is likely the broader user-facing symptom tracker for the desktop appFix branch and compare are here if useful for triage:
Closing in favor of #14950.