…s/handles)
render-mcp parked every LOVR launch in the process-lifetime AsyncExitStack, so
on each respawn the previous ManagedProcess context (its two _forward pipe
tasks + open log-file handle) was never torn down until whole-process
shutdown — N restarts leaked N-1 dead contexts.
Each launch now gets its own AsyncExitStack, closed inside _watch as soon as
the child exits and before a respawn is allowed. A single _aclose_live_launch
callback registered once on the app-lifetime stack covers the
shutdown-while-LOVR-running case, so nothing accumulates per launch. The
teardown is safe post-exit: ManagedProcess skips terminate when returncode is
already set and just cancels the pipe tasks and closes the sink.
Adds a regression test (local/gpu-marked) asserting the previous launch
context is closed on respawn and the live one is closed at shutdown.
Fixes #196.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
Fixes #196.
render-mcpleaked aManagedProcesscontext on every LOVR respawn.In
agent-mcp-servers/render-mcp/render_mcp/__main__.py, each successfulstart_lovr_oncedidawait self._stack.enter_async_context(ManagedProcess(...)), whereself._stackis the process-lifetimeAsyncExitStackfrom_serve. When LOVR exits,_watchreset_lovr_started = Falseand a laterstart_xrentered a newManagedProcessinto the same stack — so the previous context's teardown (its two_forwardpipe tasks + open log-file handle) never ran until whole-process shutdown. Across N restarts, N-1 dead contexts accumulated.Fix
AsyncExitStack(self._launch_stack); theManagedProcessis entered into that, not the app-lifetime stack._watchcloses the per-launch stack as soon as the child exits, before resetting_lovr_started(i.e. before a respawn is allowed) — running theManagedProcessteardown (cancel pipe tasks, close log sink). This is safe post-exit:ManagedProcessskipsterminate()whenreturncodeis already set._aclose_live_launchcallback is registered once on the app-lifetime stack to cover the shutdown-while-LOVR-still-running case, so nothing accumulates per launch.Net: the app-lifetime stack now holds one callback instead of one dead
ManagedProcessper restart.Test
Added
test_lovr_respawn_closes_previous_launch_context(in the existing local/gpu-markedtests/test_local_render_mcp.py, which already stubs LOVR). It asserts:_watch, the previous launch context is closed (__aexit__ran) and_launch_stackis cleared;start_lovr_onceopens a fresh context with no accumulation (exactly one closed context);No
pyproject.tomlchange (stdlib only), so noDEPENDENCIES.mdupdate.🤖 Generated with Claude Code