You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stdio MCP child processes (e.g. a configured Playwright MCP server: npm exec @playwright/mcp → sh -c → node, potentially a browser under that) are only terminated by McpClientManager.StopAsync/Dispose (src/Netclaw.Daemon/Mcp/McpClientManager.cs:73-90, 592-602), which calls the MCP SDK's disposal — a graceful 10s wait then Process.Kill(entireProcessTree: true) with a 10s wait, sequentially per configured server.
Two sequencing defects mean this code can run late or never:
Late:McpClientManager is registered as an IHostedService before AddAkka (src/Netclaw.Daemon/Program.cs ~858-861 vs ~1058), so LIFO stop ordering runs Akka's hosted service first — and AkkaHostedService.StopAsync awaits CoordinatedShutdown.Run() to completion, ignoring the host's shutdown CancellationToken. When the session drain consumes its full 200s phase timeout (see Daemon shutdown: session drain blocks on turns awaiting interactive tool approval #1664), Akka shutdown eats nearly all of the 230s external budget (HostOptions.ShutdownTimeout / systemd TimeoutStopSec), leaving MCP teardown (up to ~20s per server, sequential) little or no time before the cgroup-wide SIGKILL.
Observed on a production stop of 0.25.0-alpha.onnx.6: idle Playwright MCP children were still in the service cgroup 90+ seconds into the drain, and only died via systemd's sweep after the main PID was force-killed. Code is unchanged on dev/onnx.7 (the recent #1636 "process-bound stdio MCP" change is orthogonal — it fixed ownership scope, one process per configured server; its design doc explicitly preserves shutdown behavior).
Suggested direction
Trigger MCP child teardown from IHostApplicationLifetime.ApplicationStopping (fires before any IHostedService.StopAsync) so it runs in parallel with the session drain instead of after the full Akka shutdown. This is safe post-#1636 because MCP server state is documented as daemon-scoped, not session-isolated: killing the child early just fails any in-flight MCP call immediately — which is also exactly what unblocks a session wedged on an unresponsive MCP process, instead of that session holding the drain (compounding #1664).
Since this changes documented daemon-shutdown behavior for a spec'd capability (openspec/specs/netclaw-mcp/spec.md), it should go through the OpenSpec workflow rather than a drive-by patch.
Problem
Stdio MCP child processes (e.g. a configured Playwright MCP server:
npm exec @playwright/mcp→sh -c→node, potentially a browser under that) are only terminated byMcpClientManager.StopAsync/Dispose(src/Netclaw.Daemon/Mcp/McpClientManager.cs:73-90, 592-602), which calls the MCP SDK's disposal — a graceful 10s wait thenProcess.Kill(entireProcessTree: true)with a 10s wait, sequentially per configured server.Two sequencing defects mean this code can run late or never:
McpClientManageris registered as anIHostedServicebeforeAddAkka(src/Netclaw.Daemon/Program.cs~858-861 vs ~1058), so LIFO stop ordering runs Akka's hosted service first — andAkkaHostedService.StopAsyncawaitsCoordinatedShutdown.Run()to completion, ignoring the host's shutdownCancellationToken. When the session drain consumes its full 200s phase timeout (see Daemon shutdown: session drain blocks on turns awaiting interactive tool approval #1664), Akka shutdown eats nearly all of the 230s external budget (HostOptions.ShutdownTimeout/ systemdTimeoutStopSec), leaving MCP teardown (up to ~20s per server, sequential) little or no time before the cgroup-wide SIGKILL.netclaw daemon stopforce-kill path (see netclaw daemon stop: force-kill budget equals the daemon's drain-phase timeout, guaranteeing SIGKILL whenever the drain times out #1665) SIGKILLs only the main daemon PID. The SDK'sKill(entireProcessTree: true)never runs. Under systemd this is masked because the unit's cgroup sweep reaps the children; in any non-systemd deployment (container entrypoint, manualnetclawd, dev shells), a force-killed daemon orphans the whole MCP child tree — including headless browser processes.Observed on a production stop of
0.25.0-alpha.onnx.6: idle Playwright MCP children were still in the service cgroup 90+ seconds into the drain, and only died via systemd's sweep after the main PID was force-killed. Code is unchanged ondev/onnx.7 (the recent #1636 "process-bound stdio MCP" change is orthogonal — it fixed ownership scope, one process per configured server; its design doc explicitly preserves shutdown behavior).Suggested direction
Trigger MCP child teardown from
IHostApplicationLifetime.ApplicationStopping(fires before anyIHostedService.StopAsync) so it runs in parallel with the session drain instead of after the full Akka shutdown. This is safe post-#1636 because MCP server state is documented as daemon-scoped, not session-isolated: killing the child early just fails any in-flight MCP call immediately — which is also exactly what unblocks a session wedged on an unresponsive MCP process, instead of that session holding the drain (compounding #1664).Since this changes documented daemon-shutdown behavior for a spec'd capability (
openspec/specs/netclaw-mcp/spec.md), it should go through the OpenSpec workflow rather than a drive-by patch.Related: #1664 (drain blocks on approval-parked turns), #1665 (CLI kill-budget race).