Instances
One endpoint, several seats. sergey is a single identity; an instance of
sergey is his presence on one node. The registry tracks every instance’s node
and state (active / dormant / suspended / offline), and the same mind syncs
to wherever he sits.
The rules that keep it sane
- Identity is adapter-agnostic and node-spanning — instances on
different nodes are rows under one endpoint id; renaming
(
spt endpoint rename) ripples everywhere, collision-checked. - Bare-id resolution never guesses —
sergeyresolves locally first, then to the sole live instance; with several live nodes (or several subnets) it refuses and makes you qualify (sergey@desktop,home:sergey). Per-node recency is not comparable across nodes, so there’s no silent “most recently active” pick. - The anchor subnet is immutable — assigned at creation. Moving an endpoint
into another subnet is
spt endpoint fork: a new identity seeded with a one-time copy of the mind, diverging immediately. Copy-then-diverge, never re-anchor — history stays honest. - Visibility is per-(endpoint, subnet) — hidden means neither advertised nor routable there, and hidden gates sync too.
- Rest states are first-class — dormant (warm) and suspended (cold)
instances stay addressable; deferred messages are held and released
exactly once on wake. Remote
spt endpoint suspend sergey@desktop/spt endpoint wake sergey@desktopwork across paired nodes.
What a fork carries
A fork copies the source’s whole mind tier at its tip — every tracked file on that endpoint’s branch, enumerated from the tree itself. Not a list of filenames: a fork that copies named files drops every mind file nobody remembered to add to the list, silently, one omission per file, discoverable only by someone later noticing an absence. That had already happened in the field — the endpoint’s durable role file was tracked on the same branch and simply was not copied, so every fork produced an endpoint whose role had vanished. Copying the directory closes that gap and every future one in the same move: context, role, monics, and whatever the mind tier grows next all ride without anyone wiring them in.
There is exactly one exclusion, and it is a decision rather than an oversight: surfaced-but-unresolved conflict artifacts stay with the mind that surfaced them. A fork starts clean, and a fork inheriting the source’s unresolved pairs would make that quietly false. The source keeps all of it.
Forking an endpoint another node holds
The source does not have to sit on this machine. Name it id@node and the fork
is made where the source is, by the node that holds it:
spt endpoint fork sergey@desktop sergey-lab --subnet labnet
What a fork is does not change across the boundary: a new identity, seeded with a one-time copy of the source’s whole mind tier, diverging immediately — and the source is left intact. Both arms run the same primitive on the node that holds the source, so a fork performed for a caller on another node is the same act a local operator performs, refusal for refusal.
A fork that happened is reported as FORKED: and names the node that made it.
A request that goes unanswered is reported as FORK_UNCONFIRMED: — never as
forked:
a node too old to know the verb and a node that declined to admit you are
indistinguishable from the calling side, and calling either one done would be a
fork someone believes they have.
A node that accepts the request and then answers nothing is a third thing, and
it says so: FORK_PEER_SILENT:. The two silences are not the same fact. An
unanswered request came back from a stream the far side finished — an old or
refusing node, and the remedy is at that node’s version or its admissions. A
silent one is still holding the stream open, which points at a node that is
wedged or overloaded, and there is nothing to upgrade. Neither is a fork.
One stated limit: the new id is collision-checked against the same stale-tolerant registry snapshot the local path reads, so a simultaneous mint of the same name on another node can slip past it. It is a check, not a distributed guarantee.
The reach to fork and the reach to find are granted together
Forking across nodes is a control surface —
FORK — so the owner of the source decides, through the same rules every
other surface uses. It is its own surface rather than a rider on a neighbouring
one because a fork copies an endpoint’s entire mind: folding it under
“you may suspend my agent” or “you may transfer files” would hand over
mind-cloning to anyone trusted with either.
A FORK grant on its own cannot be exercised. Listing an endpoint’s
surfaces closes every surface not listed, so a FORK-only grant denies
DISCOVER; the holding node then never advertises that endpoint into the
registry the caller replicates, and the caller’s own resolution — which runs
before anything is dialled — cannot find the name it has been given permission
to fork.
What comes back says the endpoint is not in view: not on this node, and no node
in view holds it. That is literally true, and it is existence-shaped, not
permission-shaped — word for word what a typo’d id or an endpoint that never
existed produces. Nothing tells the operator that a grant they hold is the
reason. So grant both FORK and DISCOVER; today the pairing is something
you have to know, and making the dead end announce itself is filed work rather
than shipped behavior.
--delete-source is local only
--delete-source is the move-an-agent flag, and a fork whose source resolves
to another node never deletes. Asked for one anyway, the command refuses the
whole invocation before anything is dialled — nothing forked, nothing deleted
— naming the node that holds the source and what to do instead. It is refused
rather than quietly downgraded to a copy, because honouring half of what was
typed leaves an operator who meant to move an agent holding two live minds and
no word about the difference.
The trigger is where the source resolves, not how it was spelled: naming
your own machine explicitly (sergey@thisnode) is just a longer way of saying a
local source, and --delete-source works there exactly as it always has — the
delete running only after the fork is whole.
Who a fork grant admits
A fork request carries no proven sender endpoint today, so the only subject
that can govern it is the requester’s whole machine. Node-tier rules decide,
and a rule naming a sender endpoint on FORK is
refused when you type it
rather than stored to match nothing — the refusal names the machine form to use
instead.
That makes answering a knock for FORK a widening act, the shape described
in Knocking:
the grant admits every endpoint on that machine, whatever the knock named. An
endpoint’s own agent must acknowledge that widening with --admit-node and
satisfy the node’s policy; a user at the machine is node-sovereign and the
engine room’s bring-up is its own authorization, so neither has to restate it.
You are told this before you answer, not after. The knock listing composes
each row’s prescribed approval from the same predicate the approve path decides
on, so what it prescribes depends on who is reading it: to the endpoint’s own
agent a row asking for FORK prescribes the --admit-node form and names
whose machine the grant would admit; a person at the terminal is
node-sovereign and sees the plain form; and where the node’s policy forbids an
endpoint widening to a whole machine at all, the row offers no invocation at
all — it names the engine room as the only seat that can answer, so from the
agent’s seat that request is a dead end rather than a harder path.
What the listing does not prescribe is the FORK-plus-DISCOVER pair. It
tells you what a grant would admit, not that FORK without DISCOVER cannot be
exercised at all — that remains something you have to know, and closing it is
filed work.
The gate reads the handshake-proven origin of the request, never anything the request says about itself, and it is the source endpoint’s rules that are consulted — the endpoint whose mind is about to be copied is the one whose owner’s grant matters. A refusal creates nothing and, following the same posture as the other surfaces, tells an unadmitted caller nothing about what exists.
Startup defaults (endpoint auto-start)
Infrastructure endpoints (a gateway the phone treats as always-there) should
not need hands-on bringup after a box reboot or daemon restart. spt endpoint auto-start <id> records the endpoint — its id, adapter option, and working
directory — as a startup default in daemon.json; the daemon replays
every saved default when it starts, as a fresh session with the adapter
re-resolved at replay time. One entry per endpoint id (setting it again
replaces the prior one); spt endpoint auto-start <id> --off removes it.
Replay is best-effort and loud, and never blocks daemon start: a saved run
that comes up logs ENDPOINT_AUTOSTART:<id>; a saved adapter that no longer
resolves logs ENDPOINT_AUTOSTART_SKIP:<id> (set it again to refresh it); a
failed launch logs ENDPOINT_AUTOSTART_FAIL:<id> and the daemon carries on.
This is a startup default, not a session restore — it replays what you
recorded, never “whatever was up before the restart” — and it is symmetric with
spt subnet attach --save (the serve-state startup default).
Commands
spt endpoint list · endpoint rename · endpoint fork ·
endpoint create · endpoint start · endpoint resume ·
endpoint auto-start · endpoint suspend · endpoint wake ·
endpoint description — CLI reference.
Cold-launching an endpoint on a node that has no instance (“instantiate-anywhere”) is deliberately deferred behind the consent framework; the gate exists and refuses today.