Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 guessessergey resolves 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@desktop work 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 surfaceFORK — 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 descriptionCLI 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.