Identity coordinator — one brain, many hands
Exclusive HyperServices claim at Lookup. Workers advertise. The winning brain can publish placement advice. Clients dial through Lookup — no Hyperlink.Manager.
Handoff SSOT: docs/handoffs/identity-coordinator.md. Runnable form: examples/forms/hyperlink/node-identity-coordinator.ts.
The picture
Lookup
Identity → Router (only one live winner)
Directory → Worker#w1, Worker#w2, …
Advice → prefer Worker#w2 right nowSame yield* Router / yield* Worker everywhere. Winner serves; losers become clients of the winner; hands come and go; Lookup stays the truth.
Recipe
1. Stamp the brain
class Router extends Hyperlink.Tag<Router>()("fleet/Router", {
enqueue: Hyperlink.effectFn({ job: Job }, Schema.Void),
}).pipe(Hyperlink.identity) {}Hyperlink.identity makes layer / serve claim fleet/Router at Lookup. First live claimant serves; later claimants dial the winner. Dead winners are replaceable (Node.status ping).
2. Hold Lookup; pipe it on listens
yield* Layer.build(Lookup.layerOptions({ path: lookupSock, unlink: true }))
const lookup = Lookup.clientOptions({ path: lookupSock })
Node.unix(RouterNode, [Hyperlink.serve(Router, impl)]).pipe(Layer.provide(lookup))
Node.unix([Hyperlink.serve(Worker, impl)]).pipe(Layer.provide(lookup)) // advertiseLookup stays pipe-only on listens — never bake lookupPath into listen options.
3. Publish prefer (optional, M5)
const listen = Context.get(workerBCtx, Node.ListenNode)
yield* Lookup.prefer(Worker, listen.key) // sugar over Lookup.adviseLast write wins. Stale prefer (node not in Lookup.nodesServing(Worker)) is ignored. Directory queries use the same sugar style: yield* Lookup.nodesServing(Jobs) (Tag or wire key) — wire payload stays NodesServingRequest (serviceKey).
4. Dial hands
Hyperlink.lookupClient(Worker) // honors live Advice; else D4 { pick } / fail-closed
// or
Hyperlink.lookupClient(Worker, { pick: "first" })When identity fails closed
IdentitySelfRequired means the Tag is identity-stamped but the layer graph is missing Lookup.Identity and/or a dialable self:
Provide
Lookup.client/Lookup.layer/Lookup.layerOptions(pipe on the listen or layer).Give the Tag a dialable endpoint —
Node.unix/http/wslisten (ListenNode) orHyperlink.nodes([SomeNode])/ Tag-bound{ path }Node.
Custody vs membership (Launcher + Lookup)
Bring-up has two planes — do not collapse them:
Custody Launcher.up / Node.assume OS process Ready → self-owned
Membership Lookup Identity/Directory/Advice who wins / where clients dialLauncher stays custody-only (stable addressed node; exits after assume).
Child pipes
Lookup.client/layerOptionson listen — advertise + identity claim.Directory-row replace:
onConflict: "askIncumbent"+ optionalListenOptions.onYield(falserefuses). Drain / state / old shutdown are a later handoff track — not Lookup.
Runnable: examples/forms/hyperlink/launcher-lookup-membership.ts. Custody API: docs/guides/launcher.md.
What not to build
Do not invent
Hyperlink.Manager— identity + directory + advice is the pattern.Do not put Lookup bootstrap inside protocol listen options — pipe
Layer.provide.Do not blank-worker / remote-assign layers from Lookup — entry chooses capabilities; Lookup arbitrates membership.