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
[enhancement] Recovering from a hostname change means hand-editing machine.json — the CLI should ask, on first run, which of the account's unclaimed names are this machine #460
The recovery from a hostname change is hand-editing a JSON file
Split out of #393, whose own closing note said this belongs in its own issue rather than absorbed
there. #393's three items are delivered; this is the gap its comment thread named and its checkboxes
never covered.
What a user has to do today
#379 gave a machine a stable id and #393 taught the list to fold by it. Both work. But the name
history starts at upgrade time: a fresh ~/.config/proagentstore/machine.json holds { id, names: [<current hostname>] }, so claimMachineNames stamps exactly one row, and rows minted
under a name the machine used before it had an id can never be claimed by anything.
That is not hypothetical — it is what happened on this account, and the recovery was:
1. upgrade the CLI
2. open ~/.config/proagentstore/machine.json in an editor and add the other hostnames by hand:
{ "id": "3901c9c4-…", "names": ["Mac", "RLs-MacBook-Air.local", "RLs-MacBook-Air"] }
3. restart `pags up`
Step 2 is the product gap. It is also the only step that matters: with it, all three rows take one machine_id, aliasNodesFor proves they are one machine, six stranded pins route to the live socket,
and the fold collapses them. Without it the mechanism is correct and does nothing.
The current file on that laptop still carries the hand-written list, which is the evidence: the fix
works, and it only worked because someone edited JSON.
What to do
On first run — when the CLI has just minted an id and is about to register — ASK. The server
already knows the candidate list; GET /v1/terminals/nodes returns every node on the account with
its machineId (null = unclaimed), lastSeenAt and connected. So:
PAGS knows 3 machine names on this account that no machine has claimed.
Is this machine also known as:
[ ] RLs-MacBook-Air.local (last seen 2026-08-07, 16 agents)
[ ] RLs-MacBook-Air (last seen 2026-07-16, 2 agents)
Selecting a name merges its agents and sessions onto this machine. [Enter to skip]
Confirmed names are appended to machine.json's names and claimed on the next register. Nothing
else changes — claimMachineNames already does the work.
Only offer unclaimed names. A name already carrying a different machineId is another machine's
proven identity; it must not appear in the list at all.
Skippable and non-blocking.pags up must still come up on a headless box, over SSH, in CI, and
with no TTY. No prompt when --headless, when stdin is not a TTY, or when there is nothing
unclaimed to offer — and a skip must be remembered, not re-asked on every start.
Offer it once, not forever. Record the decline so a user who genuinely has three machines is not
interrogated at every launch.
Alternatives considered and rejected
A console-side "these are the same machine" merge control. Rejected as the first move: the
browser cannot prove anything about the machine, so it would be the same guess with a nicer surface.
The CLI is the only party that is on the machine, which is what makes its answer evidence.
Auto-claim any unclaimed name last seen from the same public IP. Rejected — a household NAT
makes two different laptops indistinguishable, and the cost of a wrong merge is another agent's
sessions and pins moving to the wrong machine.
Fold by name similarity (X and X.local). Rejected: it is right often enough to be trusted and
wrong exactly where it is expensive. .local is mDNS, not an alias guarantee.
A pags machines claim <name> command instead of a first-run prompt. Worth having as well, and
cheap once the API call exists — but it does not close the gap, because the user who needs it does
not know the command exists. The whole defect is that the remedy is invisible.
Acceptance criteria
On a machine with a fresh machine.json, pags up lists the account's unclaimed node names with
last-seen and agent counts, and claims only the ones the user selects.
Claimed names appear in machine.jsonnames and are stamped with this machine's machine_id
on the next register; GET /v1/terminals/nodes then folds them into one machine with the others
in aka.
A name already bearing a different machineId is never offered.
--headless, a non-TTY stdin, and "nothing unclaimed" each start the runner with no prompt and
no delay.
A decline is remembered and not re-asked.
No path claims a name the user did not select.
Regression risk
pags up is the entry point for every runtime agent; a prompt that blocks on a machine with no TTY
would take the whole runtime offline. The non-TTY and --headless criteria above are the guard, and
they need a test that runs the startup path with stdin closed.
Ships as a CLI release, so it reaches nobody until packages/cli/package.json is bumped and CI
publishes.
Files: packages/cli/src/machine.ts (the id + name file), packages/cli/src/up.ts (first-run path), workers/api/src/routes/terminals.ts:174-259 (the candidate list, already returns machineId/aka/ identityHint), workers/api/src/lib/machine-identity.ts (claimMachineNames, adoptableIdByName).
Follows #393 and #379.
The recovery from a hostname change is hand-editing a JSON file
Split out of #393, whose own closing note said this belongs in its own issue rather than absorbed
there. #393's three items are delivered; this is the gap its comment thread named and its checkboxes
never covered.
What a user has to do today
#379 gave a machine a stable id and #393 taught the list to fold by it. Both work. But the name
history starts at upgrade time: a fresh
~/.config/proagentstore/machine.jsonholds{ id, names: [<current hostname>] }, soclaimMachineNamesstamps exactly one row, and rows mintedunder a name the machine used before it had an id can never be claimed by anything.
That is not hypothetical — it is what happened on this account, and the recovery was:
Step 2 is the product gap. It is also the only step that matters: with it, all three rows take one
machine_id,aliasNodesForproves they are one machine, six stranded pins route to the live socket,and the fold collapses them. Without it the mechanism is correct and does nothing.
The current file on that laptop still carries the hand-written list, which is the evidence: the fix
works, and it only worked because someone edited JSON.
What to do
On first run — when the CLI has just minted an id and is about to register — ASK. The server
already knows the candidate list;
GET /v1/terminals/nodesreturns every node on the account withits
machineId(null = unclaimed),lastSeenAtandconnected. So:Confirmed names are appended to
machine.json'snamesand claimed on the next register. Nothingelse changes —
claimMachineNamesalready does the work.Constraints that are not negotiable
fail-closed is the one guarantee [bug] A machine's identity is os.hostname(), so one laptop is three machines — and a pin to a name it no longer uses answers every call with "run pags up" while pags up is running #379 exists to provide. Do not infer from MAC address, serial, or
hostname similarity —
RLs-MacBook-AirandRLs-MacBook-Air.locallook like the same machine anda rule that says so would also merge two laptops an admin named consistently.
machineIdis another machine'sproven identity; it must not appear in the list at all.
pags upmust still come up on a headless box, over SSH, in CI, andwith no TTY. No prompt when
--headless, when stdin is not a TTY, or when there is nothingunclaimed to offer — and a skip must be remembered, not re-asked on every start.
interrogated at every launch.
Alternatives considered and rejected
browser cannot prove anything about the machine, so it would be the same guess with a nicer surface.
The CLI is the only party that is on the machine, which is what makes its answer evidence.
makes two different laptops indistinguishable, and the cost of a wrong merge is another agent's
sessions and pins moving to the wrong machine.
XandX.local). Rejected: it is right often enough to be trusted andwrong exactly where it is expensive.
.localis mDNS, not an alias guarantee.pags machines claim <name>command instead of a first-run prompt. Worth having as well, andcheap once the API call exists — but it does not close the gap, because the user who needs it does
not know the command exists. The whole defect is that the remedy is invisible.
Acceptance criteria
machine.json,pags uplists the account's unclaimed node names withlast-seen and agent counts, and claims only the ones the user selects.
machine.jsonnamesand are stamped with this machine'smachine_idon the next register;
GET /v1/terminals/nodesthen folds them into one machine with the othersin
aka.machineIdis never offered.--headless, a non-TTY stdin, and "nothing unclaimed" each start the runner with no prompt andno delay.
Regression risk
pags upis the entry point for every runtime agent; a prompt that blocks on a machine with no TTYwould take the whole runtime offline. The non-TTY and
--headlesscriteria above are the guard, andthey need a test that runs the startup path with stdin closed.
claimMachineNamesstamps rows, and nothing un-stampsthem. Consider what the escape hatch is before shipping the prompt — "Forget this machine"
(
DELETE /v1/terminals/nodes/:node, [bug] One laptop still shows as three machines — nothing has claimed an id yet, the stale names can never be claimed, and the Terminals page does not fold by machine #393) removes a registration but does not un-merge a name thathas been claimed.
packages/cli/package.jsonis bumped and CIpublishes.
Files:
packages/cli/src/machine.ts(the id + name file),packages/cli/src/up.ts(first-run path),workers/api/src/routes/terminals.ts:174-259(the candidate list, already returnsmachineId/aka/identityHint),workers/api/src/lib/machine-identity.ts(claimMachineNames,adoptableIdByName).Follows #393 and #379.