#379 shipped the right mechanism. Two things stop it clearing what is already on the owner's
account, and one of them will still be true after every machine upgrades.
Live state, measured on the owner's account today (post-#379 deploy)
GET /v1/terminals/nodes
Mac connected=true lastSeen=2026-08-07 23:23 v=0.4.35 16 instances machineId=None
RLs-MacBook-Air.local connected=false lastSeen=2026-08-07 09:20 v=0.4.35 16 instances machineId=None
RLs-MacBook-Air connected=false lastSeen=2026-07-16 22:28 v=0.4.18 2 instances machineId=None
Sergeys-Mac-mini.local connected=true lastSeen=2026-08-07 23:23 v=0.4.22 4 instances machineId=None
The first three are one laptop. Every row has machine_id: NULL, so foldNodesByMachine has
nothing to fold and aliasNodesFor can prove nothing — which is exactly the fail-closed behaviour
#379 designed, working as intended.
1. Nothing has claimed an identity yet, because the machine is on an old CLI
Published: 0.4.40. Installed and running on that laptop: 0.4.35, which contains zero
occurrences of machineId in its bundle. So no registration has ever carried an id, and none will
until the CLI is upgraded and pags up restarted.
Not a bug — but worth stating plainly somewhere the user sees it, because from the console the fix
looks like it did nothing. A runner that predates the feature is indistinguishable, in the UI, from
one that has it and found nothing to merge.
2. After the upgrade, the two stale rows still will not merge — by construction
machine.ts's own docstring says it:
"on the first run the machine can only claim the hostname it already registered under, which
changes nothing. The moment the network renames it, the list holds BOTH names"
The name history starts at upgrade time. A fresh machine.json on that laptop will hold
{ id, names: ["Mac"] }, so claimMachineNames stamps the Mac row and nothing else.
RLs-MacBook-Air.local and RLs-MacBook-Air were minted before any id existed and no machine will
ever claim them — they are permanent residue, listed forever as offline machines the user
recognises as their own laptop.
That is the correct trade (guessing costs the guarantee), and it leaves the account needing a
manual escape that does not exist.
There is no way to forget a node. No route, no control. The only listed options are to leave
them or edit D1 by hand.
3. The picker folds; the Terminals page does not
GET /v1/instances/:id/runner-node folds correctly — instances.ts:421 calls
foldNodesByMachine, with the reasoning in a comment.
GET /v1/terminals/nodes does not: grep machine_id workers/api/src/routes/terminals.ts
returns nothing, and groupTerminalNodes groups by the runner_node string. That endpoint feeds
both the Terminals page and the "Runs on" tiles in RunnerPanel (machinesToShow ←
/v1/terminals/nodes), which is the surface the duplicates were reported on.
So once ids do exist, the two surfaces will disagree: the picker's dropdown shows one machine and
the tiles beside it still show three.
Do
While we are here: "one pags up per machine" is not what is enforced
The 409 lives at the RelayDO and is keyed per (instance, node NAME) — a second runner on the
same hostname for the same instance is rejected unless --force. With three hostnames, the
platform sees three machines and nothing ever conflicted. Node rows are also never deleted on
disconnect, so each identity persists after its name stops being used. Both behaviours are correct
in isolation; together they are why one laptop can occupy three slots and no guard notices.
Follows #379 (the mechanism) and #380. Evidence gathered live via the MCP server + REST on the
owner's account, 2026-08-08.
#379 shipped the right mechanism. Two things stop it clearing what is already on the owner's
account, and one of them will still be true after every machine upgrades.
Live state, measured on the owner's account today (post-#379 deploy)
The first three are one laptop. Every row has
machine_id: NULL, sofoldNodesByMachinehasnothing to fold and
aliasNodesForcan prove nothing — which is exactly the fail-closed behaviour#379 designed, working as intended.
1. Nothing has claimed an identity yet, because the machine is on an old CLI
Published: 0.4.40. Installed and running on that laptop: 0.4.35, which contains zero
occurrences of
machineIdin its bundle. So no registration has ever carried an id, and none willuntil the CLI is upgraded and
pags uprestarted.Not a bug — but worth stating plainly somewhere the user sees it, because from the console the fix
looks like it did nothing. A runner that predates the feature is indistinguishable, in the UI, from
one that has it and found nothing to merge.
2. After the upgrade, the two stale rows still will not merge — by construction
machine.ts's own docstring says it:The name history starts at upgrade time. A fresh
machine.jsonon that laptop will hold{ id, names: ["Mac"] }, soclaimMachineNamesstamps theMacrow and nothing else.RLs-MacBook-Air.localandRLs-MacBook-Airwere minted before any id existed and no machine willever claim them — they are permanent residue, listed forever as offline machines the user
recognises as their own laptop.
That is the correct trade (guessing costs the guarantee), and it leaves the account needing a
manual escape that does not exist.
There is no way to forget a node. No route, no control. The only listed options are to leave
them or edit D1 by hand.
3. The picker folds; the Terminals page does not
GET /v1/instances/:id/runner-nodefolds correctly —instances.ts:421callsfoldNodesByMachine, with the reasoning in a comment.GET /v1/terminals/nodesdoes not:grep machine_id workers/api/src/routes/terminals.tsreturns nothing, and
groupTerminalNodesgroups by therunner_nodestring. That endpoint feedsboth the Terminals page and the "Runs on" tiles in
RunnerPanel(machinesToShow←/v1/terminals/nodes), which is the surface the duplicates were reported on.So once ids do exist, the two surfaces will disagree: the picker's dropdown shows one machine and
the tiles beside it still show three.
Do
/v1/terminals/nodesbymachine_id— reusefoldNodesByMachine; the rows alreadycarry the column. Without this the fix is invisible on the page it was reported from.
delete-on-stale sweep: a laptop that is merely closed must not lose its pins). Needs a route
and a control on the Terminals page. Guard the case where the node still holds pins or
coding_sessions.runner_noderows — offer to repoint them, or refuse and say why.runner_version; arow whose version predates machine-id support could read "this machine's CLI is 0.4.35 —
upgrade to merge it with its other names", which turns an invisible no-op into an
instruction.
While we are here: "one
pags upper machine" is not what is enforcedThe 409 lives at the RelayDO and is keyed per (instance, node NAME) — a second runner on the
same hostname for the same instance is rejected unless
--force. With three hostnames, theplatform sees three machines and nothing ever conflicted. Node rows are also never deleted on
disconnect, so each identity persists after its name stops being used. Both behaviours are correct
in isolation; together they are why one laptop can occupy three slots and no guard notices.
Follows #379 (the mechanism) and #380. Evidence gathered live via the MCP server + REST on the
owner's account, 2026-08-08.