Summary
A long-running docker-agent run process leaks unix-domain socket file descriptors steadily. A 9.5-hour session was observed holding 1,028 open unix-domain sockets (FD numbers contiguously spanning 6→1071), climbing toward the process FD ceiling.
Root cause
pkg/desktop/raw_client.go: RawClient.client is a func() *http.Client that constructs a brand-new http.Client + bare http.Transport on every call. Each call dials Docker Desktop's backend.sock. With HTTP keep-alives enabled and no IdleConnTimeout set, the connection is parked in the throwaway transport's idle pool and pinned indefinitely by its persistConn goroutines → 1 leaked unix FD (+2 goroutines) per call.
Primary trigger — one leak per LLM request (Gateway mode)
In Docker AI Gateway mode, per-request auth-token closures call base.GatewayAuthToken on every request, then unmemoized desktop.GetToken and ClientBackend.Get.
Secondary drips include 1-minute /ping probes, unmemoized IsDockerDesktopRunning, and 5-minute GetVersion.
MCP client pooling and stdio child reaping were verified correct and are not the cause.
Impact
Long-lived sessions accumulate unix-socket FDs and goroutines without bound, risking EMFILE / “too many open files”.
Proposed fix
Build the RawClient HTTP client/transport once and reuse it, with a bounded idle pool (for example MaxIdleConns: 2, IdleConnTimeout: 90s). Optionally memoize desktop.GetToken and cache the gateway auth client per provider. Add a regression test asserting open unix-socket connections stay flat across many Get calls.
Evidence
A 48-second sampling window showed FD count flat at 1028 while mid-turn; the leak is tied to discrete LLM requests. FD breakdown: 1,028 unix sockets of 1,088 total FDs, zero established TCP; RSS was ~450MB.
Summary
A long-running
docker-agent runprocess leaks unix-domain socket file descriptors steadily. A 9.5-hour session was observed holding 1,028 open unix-domain sockets (FD numbers contiguously spanning 6→1071), climbing toward the process FD ceiling.Root cause
pkg/desktop/raw_client.go:RawClient.clientis afunc() *http.Clientthat constructs a brand-newhttp.Client+ barehttp.Transporton every call. Each call dials Docker Desktop'sbackend.sock. With HTTP keep-alives enabled and noIdleConnTimeoutset, the connection is parked in the throwaway transport's idle pool and pinned indefinitely by itspersistConngoroutines → 1 leaked unix FD (+2 goroutines) per call.Primary trigger — one leak per LLM request (Gateway mode)
In Docker AI Gateway mode, per-request auth-token closures call
base.GatewayAuthTokenon every request, then unmemoizeddesktop.GetTokenandClientBackend.Get.Secondary drips include 1-minute
/pingprobes, unmemoizedIsDockerDesktopRunning, and 5-minuteGetVersion.MCP client pooling and stdio child reaping were verified correct and are not the cause.
Impact
Long-lived sessions accumulate unix-socket FDs and goroutines without bound, risking
EMFILE/ “too many open files”.Proposed fix
Build the
RawClientHTTP client/transport once and reuse it, with a bounded idle pool (for exampleMaxIdleConns: 2,IdleConnTimeout: 90s). Optionally memoizedesktop.GetTokenand cache the gateway auth client per provider. Add a regression test asserting open unix-socket connections stay flat across manyGetcalls.Evidence
A 48-second sampling window showed FD count flat at 1028 while mid-turn; the leak is tied to discrete LLM requests. FD breakdown: 1,028 unix sockets of 1,088 total FDs, zero established TCP; RSS was ~450MB.