Summary
When more than one argent tool-server runs on the same host (a second dev
instance, a stale/orphaned instance, or another agent session's server), the
native-devtools injection sockets become a three-way hazard:
- Attach-to-every-sim: each tool-server's simulator watcher initializes a
NativeDevtools service — and binds /tmp/argent-nd-<UDID>.sock — for
every booted simulator, even devices that server will never drive. A
session doing Android-only work through a dev tool-server knocked out an
unrelated iOS session's devtools on a different simulator.
- Latest-bind-wins: the blueprint unlinks any existing socket file before
binding (fs.unlinkSync(socketPath) in native-devtools.ts, "remove stale
socket file from a crashed previous run"). A live server's socket is
indistinguishable from a crashed one, so every server start silently steals
all future app connections; the losing server keeps its established
connections but every relaunched app connects to the thief.
- Unlink-on-stop kills survivors: when the last-binding server stops, its
cleanup removes the socket file — the surviving servers' listeners are gone
from the filesystem, so no app can connect to anyone until something
rebinds.
Observed failure signatures (all reproduced live, 2026-08-11)
- flow
launch step: could not connect to native devtools for <bundleId>… —
the app connected to another server's socket; the flow's server waits out its
connect window in vain. Reproduced on demand: the failure windows correlate
1:1 with another server instance's start timestamps (22:07, 22:10:57,
22:15:51, 22:16:55 across two independent sessions).
ViewInspector RPC timed out: <method> mid-flow — when the steal happens
between a flow's app relaunch and its first RPC.
- With three sessions (two scoped experiments + one unrelated Android A/B loop
restarting its dev server per run), no session could keep a stable devtools
connection on any simulator.
Also worth noting operationally: there is no in-band way to identify or
negotiate with the process holding the socket — lsof + process-tree forensics
was the only way to find the owner.
Sketch of remedies (not prescriptive)
- Scope: only claim devtools sockets for devices a server is actually asked to
drive (lazy claim on first tool call for that UDID), or honor an explicit
device allowlist. (An ARGENT_DEVTOOLS_ONLY_UDID env filter prototype made
two concurrent sessions coexist cleanly during this investigation.)
- Ownership: before unlinking an existing socket, probe it — a live listener
answers; only unlink dead sockets. On shutdown, only unlink a socket the
server still owns (compare inode/identity).
- Diagnosability: embed owner pid + port in a sidecar file next to the socket
so the "stale or duplicate argent server" error can name the actual owner.
Environment
argent 0.20.0 (installed) + worktree dev builds of the tool-server, macOS,
iOS 26.4/26.5 simulators, Expensify NewDot test app. Full timeline and process
trees available on request.
Summary
When more than one argent tool-server runs on the same host (a second dev
instance, a stale/orphaned instance, or another agent session's server), the
native-devtools injection sockets become a three-way hazard:
NativeDevtools service — and binds
/tmp/argent-nd-<UDID>.sock— forevery booted simulator, even devices that server will never drive. A
session doing Android-only work through a dev tool-server knocked out an
unrelated iOS session's devtools on a different simulator.
binding (
fs.unlinkSync(socketPath)innative-devtools.ts, "remove stalesocket file from a crashed previous run"). A live server's socket is
indistinguishable from a crashed one, so every server start silently steals
all future app connections; the losing server keeps its established
connections but every relaunched app connects to the thief.
cleanup removes the socket file — the surviving servers' listeners are gone
from the filesystem, so no app can connect to anyone until something
rebinds.
Observed failure signatures (all reproduced live, 2026-08-11)
launchstep:could not connect to native devtools for <bundleId>…—the app connected to another server's socket; the flow's server waits out its
connect window in vain. Reproduced on demand: the failure windows correlate
1:1 with another server instance's start timestamps (
22:07,22:10:57,22:15:51,22:16:55across two independent sessions).ViewInspector RPC timed out: <method>mid-flow — when the steal happensbetween a flow's app relaunch and its first RPC.
restarting its dev server per run), no session could keep a stable devtools
connection on any simulator.
Also worth noting operationally: there is no in-band way to identify or
negotiate with the process holding the socket —
lsof+ process-tree forensicswas the only way to find the owner.
Sketch of remedies (not prescriptive)
drive (lazy claim on first tool call for that UDID), or honor an explicit
device allowlist. (An
ARGENT_DEVTOOLS_ONLY_UDIDenv filter prototype madetwo concurrent sessions coexist cleanly during this investigation.)
answers; only unlink dead sockets. On shutdown, only unlink a socket the
server still owns (compare inode/identity).
so the "stale or duplicate argent server" error can name the actual owner.
Environment
argent 0.20.0 (installed) + worktree dev builds of the tool-server, macOS,
iOS 26.4/26.5 simulators, Expensify NewDot test app. Full timeline and process
trees available on request.