Reported
pags up is running, but the agent's Settings shows RLs-MacBook-Air twice, both offline.
Why doesn't it show online? Should we limit clients to one per machine?
Answer to the second question first: no — one-per-machine is already enforced (the RelayDO
rejects a second runner on the same hostname with 4409 unless --force). There are not two clients.
There is one machine wearing three names, and the agent is pinned to one it stopped using.
Measured, live, on this account
$ hostname Mac
$ scutil --get ComputerName RL's MacBook Air
$ scutil --get LocalHostName RLs-MacBook-Air
$ ps pags runner connect <17 instance ids> (PID 30591, started 06:39 local)
16 ESTABLISHED sockets → 2606:4700:…:443
GET /v1/terminals/nodes — four "machines", which are really two:
| node |
connected |
last seen |
Mac |
true (16 instances) |
now |
RLs-MacBook-Air.local |
false (16 instances) |
2026-08-07 09:20 |
RLs-MacBook-Air |
false (2 instances) |
2026-07-16 |
Sergeys-Mac-mini.local |
true (4 instances) |
now |
The first three are the same laptop. It has minted a new identity every time os.hostname()
changed — which on macOS it does, because hostname follows the DHCP/network-supplied name and
flips between the .local mDNS form and whatever the network hands out. ComputerName and
LocalHostName are stable; hostname() is the one field the runner uses and the one that moves.
GET /v1/instances/cda75e28…/runner-node:
{ "runnerNode": "RLs-MacBook-Air.local",
"nodesDetail": [ { "node": "Mac", "connected": true, "nodeOnline": true },
{ "node": "RLs-MacBook-Air.local", "connected": false, "nodeOnline": false } ] }
GET /v1/relay/cda75e28…/status?node=Mac → {"connected":true}.
So the socket is up. The pin names a machine that no longer exists.
What that costs
getBoundRunnerConn (lib/runner-client.ts:80-87) treats a pin as authoritative and deliberately
does not fall back — correct, and its comment says why: falling back would run the agent "on a
machine the user did not choose, in a different checkout". Pinned + dead → null.
Every runner-backed call on that instance therefore returns:
terminal_list_targets → "No runner is connected for this agent — run `pags up` on the machine
whose terminal you want to control."
…while pags up is running, on that machine, with a live socket, 16 instances attached. The one
remedy the platform offers is the one thing the user has already done. This is the root cause of
#378 (the Terminal tab); that ticket is about how badly the state is presented, this one is why
the state exists at all.
Nothing heals it and nothing reaps the dead rows: a node row from 2026-07-16 is still in the picker
three weeks later, indistinguishable from a machine that is merely asleep.
Why "both duplicates are offline" is the honest reading of the screen
machineTile (store/console/src/lib/runnerPanel.ts) renders one tile per runner_node string.
The two tiles the user recognises as their laptop — RLs-MacBook-Air and RLs-MacBook-Air.local —
are both genuinely offline. The tile that is their laptop is called Mac, which reads as some
other machine. The picker has no notion that a machine can be renamed, so it cannot say so.
Suggested direction
- Give a machine a stable identity. Mint a UUID once, persist it next to the session
(~/.config/proagentstore/), and send it as the node key; keep hostname() as a mutable
display label. normalizeRunnerNode (lib/runtime-nodes.ts) is a trim+slice over a
user-visible string being used as a primary key — that is the whole defect in one line.
- Heal or flag a pin whose machine is gone.
/runner-node already returns everything the
sentence needs: pinned node offline, exactly one live node. Either auto-repoint with a notice, or
say it out loud — "pinned to RLs-MacBook-Air.local, last seen 21h ago; this machine now reports
as Mac — repin?". Silent-and-authoritative is right for routing and wrong for the UI.
- Reap or fold stale nodes. A node not seen for N days should leave the picker (or offer
"forget this machine"). Merging on a stable id makes this mostly unnecessary.
Workaround today: Settings → Runner → Runs on → pick Mac, or set it to Automatic (unpinned
routing already scans for whichever node holds a live socket — getLiveRunnerConn).
Related: #378 (the surface that shows this state), #238 (/runtime/status and the stale default
row), #241 (a stale offline verdict outliving its sessions).
Files: packages/cli/src/commands/runner/relay.ts:27,213, packages/cli/src/commands/runner/http.ts:53,
workers/api/src/lib/runtime-nodes.ts, workers/api/src/lib/runner-client.ts:73-116,
workers/api/src/routes/terminals.ts, store/console/src/lib/runnerPanel.ts,
store/console/src/components/RunnerPanel.tsx.
Reported
Answer to the second question first: no — one-per-machine is already enforced (the RelayDO
rejects a second runner on the same hostname with 4409 unless
--force). There are not two clients.There is one machine wearing three names, and the agent is pinned to one it stopped using.
Measured, live, on this account
GET /v1/terminals/nodes— four "machines", which are really two:MacRLs-MacBook-Air.localRLs-MacBook-AirSergeys-Mac-mini.localThe first three are the same laptop. It has minted a new identity every time
os.hostname()changed — which on macOS it does, because
hostnamefollows the DHCP/network-supplied name andflips between the
.localmDNS form and whatever the network hands out.ComputerNameandLocalHostNameare stable;hostname()is the one field the runner uses and the one that moves.GET /v1/instances/cda75e28…/runner-node:{ "runnerNode": "RLs-MacBook-Air.local", "nodesDetail": [ { "node": "Mac", "connected": true, "nodeOnline": true }, { "node": "RLs-MacBook-Air.local", "connected": false, "nodeOnline": false } ] }GET /v1/relay/cda75e28…/status?node=Mac→{"connected":true}.So the socket is up. The pin names a machine that no longer exists.
What that costs
getBoundRunnerConn(lib/runner-client.ts:80-87) treats a pin as authoritative and deliberatelydoes not fall back — correct, and its comment says why: falling back would run the agent "on a
machine the user did not choose, in a different checkout". Pinned + dead →
null.Every runner-backed call on that instance therefore returns:
…while
pags upis running, on that machine, with a live socket, 16 instances attached. The oneremedy the platform offers is the one thing the user has already done. This is the root cause of
#378 (the Terminal tab); that ticket is about how badly the state is presented, this one is why
the state exists at all.
Nothing heals it and nothing reaps the dead rows: a node row from 2026-07-16 is still in the picker
three weeks later, indistinguishable from a machine that is merely asleep.
Why "both duplicates are offline" is the honest reading of the screen
machineTile(store/console/src/lib/runnerPanel.ts) renders one tile perrunner_nodestring.The two tiles the user recognises as their laptop —
RLs-MacBook-AirandRLs-MacBook-Air.local—are both genuinely offline. The tile that is their laptop is called
Mac, which reads as someother machine. The picker has no notion that a machine can be renamed, so it cannot say so.
Suggested direction
(
~/.config/proagentstore/), and send it as the node key; keephostname()as a mutabledisplay label.
normalizeRunnerNode(lib/runtime-nodes.ts) is a trim+slice over auser-visible string being used as a primary key — that is the whole defect in one line.
/runner-nodealready returns everything thesentence needs: pinned node offline, exactly one live node. Either auto-repoint with a notice, or
say it out loud — "pinned to RLs-MacBook-Air.local, last seen 21h ago; this machine now reports
as Mac — repin?". Silent-and-authoritative is right for routing and wrong for the UI.
"forget this machine"). Merging on a stable id makes this mostly unnecessary.
Workaround today: Settings → Runner → Runs on → pick
Mac, or set it to Automatic (unpinnedrouting already scans for whichever node holds a live socket —
getLiveRunnerConn).Related: #378 (the surface that shows this state), #238 (
/runtime/statusand the stale defaultrow), #241 (a stale offline verdict outliving its sessions).
Files:
packages/cli/src/commands/runner/relay.ts:27,213,packages/cli/src/commands/runner/http.ts:53,workers/api/src/lib/runtime-nodes.ts,workers/api/src/lib/runner-client.ts:73-116,workers/api/src/routes/terminals.ts,store/console/src/lib/runnerPanel.ts,store/console/src/components/RunnerPanel.tsx.