Endpoint discovery
Request routes
One kind, ep, carries every request under a mode token that says where the request
routes, never which verb it is (the verb rides the envelope): one (queue-group
anycast, one and only one class member), all (scatter, every instance), and inst (one instance by its
stable address). Replies come back on a reply rail keyed to the serving instance and its
epoch. Around these sit the sibling planes the composites use: per-goal events, timers,
sessions, and the journal that holds durable facts. Every request carries the caller as
three forge-locked tokens, owner, actor, and lifecycle uid, plus an unguessable
nonce, so the broker polices who is calling in the subject grammar itself. See
SPEC §13.2 for the grammar and §13.5 for
the verbs (call, cast, watch, claim, scatter).
Lifecycle identity
A principal owner.actor is a reusable routing alias: a despawn frees the actor name and a
later spawn may legitimately reuse it, so the alias alone is never authority. Two further
coordinates make an identity durable: a lifecycle uid, an unguessable, never-reused id
for one managed lifecycle under a principal, and a process epoch, the fenced ownership
epoch of the process currently animating it, advanced on every restart or takeover. At most
one live epoch owns an identity, and a superseded epoch must stop serving. Durables and
credentials key on the lifecycle uid, not the reusable name, which is what lets a
supervised restart recover the same lifecycle instead of minting a new one. See
SPEC §13.1 and identity & auth.
Service discovery
No client has compile-time knowledge of any endpoint's commands. cotal describe <endpoint> resolves a registered endpoint's command set off the wire: the reserved
describe command answers the registered contract digests, the schemas are fetched from the
space's content-addressed contract store, recompiled, and verified against those digests.
Each command prints with its capability class and targeting shape. cotal invoke <endpoint> <command> --args '<json>' then calls one command by name, validating the arguments as they
will be sent (JSON drops a key whose value is undefined) against the fetched input schema before
publish. A refusal at that check means nothing was sent, and it is marked not-executed. An
endpoint that refuses a request before running the command states not-executed in the reply's
error.outcome too, for example on a wrong envelope version, sender, contract digest or target
mode, or a body that does not parse. A refusal the command itself raises carries only the
outcome it states. A refusal that does not serialize comes back as internal, and one too large
to publish as resource-exhausted, and both keep that outcome. A
signed-in user invokes the same surface through their bearer, and the broker enforces each
command's existing capability grant. A manager alias supplied through --name resolves through
its name-keyed inspect command, so an authorized targeted call does not need the manager-wide
ps enumeration grant. Every built-in manager command uses this
same trust chain, so there is nothing the built-ins can reach that a described contract cannot. The registered
auth endpoint is describable the same way manager is: cotal describe auth lists
retire-lifecycle and its exact-mode target shape.
See SPEC §13.7 and cli.md.
Listing what a space offers
An agent asks the space's registry what it can call with the cotal_endpoints tool. The registry
is a registered endpoint named registry. Its endpoints command lists each registered endpoint
where the caller holds at least one command, with the commands and request forms the caller's
recorded ceiling covers, and leaves the other endpoints out. It reads the ceiling the issuer
recorded for the caller's credential and does not check that the ceiling's sources are still live,
so the broker or the endpoint can still refuse a listed command. The answer says this with
source: "recorded-ceiling". On an open mesh every registered endpoint is listed and source is
no-permission-system. Each endpoint row also lists its registered instances, the state each one
recorded, and whether its command declarations verified. A page holds up to 100 endpoints. When
the registry does not answer, the tool says so and lists nothing; it does not build a list from
describe or presence. A listing drops the session's cached describe of every endpoint it lists.
On a user-auth mesh manager commands ride a separate manager-call credential, so
cotal_endpoints with view: "manager" opens that credential as a manager call does and lists what
it can call. A session whose bearer comes from its host has no manager-call credential, and the tool
says so. See SPEC §13.20.
Machine vitals
The manager's vitals command reports one invocation-time sample of host CPU count and load,
available and total memory, and bounded local filesystem capacity. It is untargeted but resolves to
one exact Manager instance. The separate manager.vitals capability is issued only when the actor
scope contains machine-vitals; spawn, run and admin do not imply it. The response contains
mount points and aggregate byte counts, never device paths, remote endpoints, process data, command
lines, environment values or persisted history. Unsupported measurement refuses without a partial
or zero-valued fallback.
Host directory resolution
The manager's resolve-cwd command is in the manager.spawn capability class. It accepts an
absolute path on that manager's host and returns its canonical directory plus the host name. It
refuses a relative, missing or non-directory path with failed-precondition; it creates nothing.
spawn applies the same check at admission, before any credentials or durables are minted. It is
minted beside spawn in the operator instruments, which are the caller on a static mesh, and in a
spawn holder's manager-caller view. The standing agent spawn capability does not carry it.
A runtime that delegates seats outside the manager's host cannot use a host cwd. Its manager
refuses resolve-cwd with failed-precondition before any filesystem lookup, with the same
execution-mode refusal for every path. It returns no host name or canonical path.
Spawn input resolution
The manager's resolve-agent command is minted beside spawn in the spawn capability and the
operator instruments. It accepts the persona fields spawn takes (name, config, agent,
defaultAgent) and returns {agent}, the harness spawn on that manager would launch: the flag,
then the persona's agent: pin, then the caller's default, then the manager's own default. It loads
the persona with the same visibility check as spawn and launches nothing. A detached --resume
without --agent asks it before carrying a session, and a manager that does not serve it refuses
the call.
On a delegated runtime, both resolve-agent and spawn refuse an explicit persona config
before any lookup of that path. Absolute, relative and bare-name config overrides receive the
same failed-precondition refusal. Select a named catalog persona through name instead.
Locally executing managers retain their config-path behavior. A resolved manifest uses its own
validated, host-materialized persona resource and is not a caller-selected config override.
spawn refuses an empty or whitespace-only value in any of its optional string fields, such as
role, agent or identity, with bad-request naming the field. To take the persona file's value,
omit the field.
Instance routing
A space can run more than one manager. Each manager persists a stable logical instance id
across restarts and advances its process epoch when it comes back, so callers address a
specific manager without caring which process currently serves it. A start serves only at the
epoch its own registration committed, never at the epoch of a later start of the same instance.
On a static mesh or from an unbound open caller, an untargeted spawn rides class anycast
(any manager may accept, and the acceptance records which one did). An open managed seat's
persona definition, list, show and spawn tools default to its launching manager. Its generic
manager describe and self-targeted invoke use that launch address too. An explicit existing
persona or spawn selector still selects that instance. Static credential grants and user-auth
issuer selection are unchanged. For self-stop, persona definition and direct spawn, a static managed
seat keeps its class-only credential and accepts only its launching manager's attributed describe
reply. These commands retain that bind on the class rail, so a sibling refuses before effects and
the existing bounded split repair asks again. An absent launching manager never selects a replacement.
The library's describeService accepts an optional instanceId;
a pinned describe does not read or populate the endpoint-only class cache.
cotal spawn <persona> --detach --on <instance> and cotal_spawn(instance: "<instance>")
pin one instance by its exact id, as --on does for cotal ps, cotal models and every hosted
cotal run verb, and instance for cotal_run, cotal_persona and cotal_personas. A foreground CLI spawn has no manager to pin and refuses the
flag. An MCP pin that does not resolve is refused without falling back to class anycast. There are no ordinal
aliases and no short forms: wherever a display names an instance you can address, it prints
the whole id, because both surfaces take nothing else.
On a user-auth mesh, manager commands obtain a short-lived manager-caller view from the
exchange. It authorizes one concrete manager instance using the caller's current actor grant and
the host's registered service records. Discovery and invocation both use that instance's inst
route. This view grants no registry scan or class queue. It adds no command capability except the
three reads a launch needs: a view whose actor holds spawn may call status, models and
resolve-cwd on that instance, subject to the execution-mode limit above. A run-only or no-scope view may not, and ps and slots stay with
admin. A user mesh's co-located manager answers every owner's view, so its status counts only the
caller's own seats, as ps would list them, unless the caller holds admin. An absent,
ambiguous or unauthorized selection refuses before the command is sent. An ambiguous one lists the
owner's live manager instance ids and names the caller's way to pick one: --on <instanceId> for
the CLI, --manager-instance <instanceId> for cotal agent-bearer --manager-call, and the
instance argument for an agent's tools.
A pin changes routing, not the caller's grant.
Each status.connectors row includes eventPlane: boolean. The value is true only when the
connector declares the event channel it publishes. Installed-extension rows carry the cached fact
before the package is imported, and an old inventory without it is refused rather than treated as
true. A client that reads a required event policy can use this field to disable an incompatible
connector before spawn. The Manager still owns the launch refusal text and applies it at admission.
Managed launches carry COTAL_MANAGER_INSTANCE so their tools address the manager that launched
them; a tool's instance argument must name that same manager. The cotal commands run from their
shell select it too, unless --on names another instance. Existing unbound sessions can use the exchange's unique authorized selection without
replacing their actor or conversation. The connector uses a separate control connection; the
standing message connection and its credential source are unchanged. Accepted spawn goals are
followed on that renewing connection, using its existing caller-scoped progress grant, so a long
readiness budget does not depend on the short-lived control credential. The follower confirms its
progress subscription with the broker before submitting on the separate connection. A caller still
checks the resolved instance and epoch, and never retries an ambiguous mutation outcome.
The reference cotal_spawn tool retains the published request id when its submission wait
ends before acceptance is observed. That id is the manager's goal id if the submission is
accepted later. The tool reports an unknown outcome with the id and a same-caller
goal-result read, without claiming that acceptance occurred. Do not repeat the spawn.
An unanswered describe or a broker-attested no-responder refusal does not create a goal
handle. A missing readback is inconclusive while the original submission may still arrive.
Goal result reads
The manager's goal-result command accepts {goalId} and returns {goalId, result?}. It reads
only the authenticated caller's owner, actor and lifecycle through the manager's separate trusted
goal-writer connection. The caller receives an attributed reply, never a raw JetStream reader
grant. Each read is admitted by the connection's broker-enforced command grant. A live user-auth
connection remains bounded by its bearer expiry after revocation; a renewed connection is checked
against fresh authority. There is no separate per-read ledger check. An absent result means no
terminal is recorded; it does not prove the goal is running or permit another submission. The
existing trusted goal-writer's leader-served EPF read is space-wide at the broker; the handler
confines it to this endpoint and caller triple.
Goal cancellation
The manager's reserved cancel command accepts {goalId, mode?} and returns {goalId, state}.
It is served for a turn the manager relays. The goal is the authenticated caller's own, so a caller
withdraws only a turn it submitted. The turn ends cancelled, its seat is not shown it again, and a
later yield of it is answered with that terminal. A goal that already ended is refused
failed-precondition with its cached outcome attached, and a goal this manager does not relay is
refused without being changed. So is a second cancel that arrives while a first is still ending the
turn; a first that fails leaves the turn pending unless something ended it meanwhile. A workflow
run sends it for the turn, ask attempt or escalation of a branch it cancelled.
Request outcomes
A followed mutation requires a manager whose attributed describe includes goal-result. If it
is absent, preflight refuses with failed-precondition, an upgrade notice and not-executed
before submitting the mutation. Narrowed discovery preserves that refusal. Update
the manager, issuer and client together before using that recovery path. Reloading an issuer alone
cannot change an already-running participant manager. Recovery re-resolves the accepting instance's
epoch, preserves the caller lifecycle and validates the result against the accepted goal and any
acceptance fingerprint. Stopping the caller ends its observation, not the already accepted goal.
A followed call resolves the endpoint within its deadline before the submission starts, so a
refused or unanswered describe surfaces as its own error. On a user-auth mesh that preparation also
obtains the caller's manager-caller view and opens its connection, and a failure there surfaces
unchanged. The control connection uses the remaining preparation deadline, including time spent
obtaining the bearer, rather than a separate two-second limit. A deadline that passes during those
steps reports that the request was not run. The
describe then spends what is left of the deadline, so a describe that draws no reply still reports
itself as unanswered.
A describe or command publish that the broker refuses reports not-executed.
Cancellation before submission reports not-executed. Once submission starts, cancellation or a
lost reply reports an unknown outcome unless an attributed refusal proves otherwise. A received
refusal remains a refusal even when stop races it. Local failures do not invent responder identities.
The follower owns its subscription, timers and read cancellation signal. Reconciliation begins
before the wait deadline, and late read completions cannot settle an expired observation. Its read
callback receives the accepting caller triple, remaining budget and abort signal; borrowed bearer
commands and control connections use that signal. An in-flight dial that finishes after cancellation
closes without publishing. A local reply-subscription failure prevents publication and is observed
by the same request promise, including when the transport is closing or draining. Request
cancellation does not revoke or resubmit the accepted operation.
Split topology
"Only one manager per space" is not the current invariant. A split topology that keeps the
broker host manager-free is still a topology choice: cotal up --no-manager on that host starts no
manager, and cotal supervise --server runs the manager elsewhere (Run a mesh).
A broker host that already runs a manager stops it only after ✓ manager up in
.cotal/manager.<spaceKey>.log (detach stdout listing manager is pidfile liveness, not a
teardown boundary). A controller stack stops it with
cotal service stop stack-manager --space <s> --yes; a legacy stack has no command that stops only
its manager. Extra live managers are addressable, not an error.
Discovery deadlines
The reserved describe bootstrap is the one request the resolver may repeat while waiting: it is
read-only, it is re-published under the same request binding, and every attempt stays inside the
original deadline. This covers the startup window where Core NATS discards the first request before
the manager has subscribed. If the connection closes while the resolver waits, the describe fails
cleanly instead of throwing from the retry timer. The resolved command is never repeated by this
readiness behavior.
The describe and the contract store reads after it share the resolver's one deadline, so a slow
store read fails the resolve with deadline-exceeded when that deadline expires. cotal invoke passes its
--timeout as that deadline (default 10000 ms) and gives the reply what the resolve leaves of it.
cotal describe has no --timeout and passes 10000 ms. Cancelling a
resolve settles a store read in flight instead of waiting for it to return. When one store read
fails, the resolve fails with it and fetches no further command contracts; fetches already under
way finish on their own. A call for one command fetches only that command's cluster and schema
closures. cotal invoke --name also fetches inspect's, because it resolves the target through
inspect. describe still fetches the complete visible surface because its result lists every
command.
Instance mismatch
The resolve and the invoke are separate trips through the same anycast queue, so in a
multi-manager space an unpinned call can land on an instance the caller did not resolve. Every
call carries the incarnation it resolved against, and a manager that is not that incarnation
refuses before running the command, so the failure an operator sees says the command did
not run, and re-issuing it cannot duplicate the effect. That is the difference that matters for
a mutation: the older behaviour detected the mismatch on the reply, after the manager had
already acted, and could only tell you to go and check. --on still matters for reaching a
specific manager (ps, stop, attach, spawn --detach), but it is no longer what stands
between a split and a duplicated spawn. Against a manager older than this fence the refusal is
still after the fact, and its message says so. The re-issue is automatic only when the refusal
states not-executed in its outcome field; a refusal that omits the field, or states
unknown, is surfaced to the caller instead of repaired, because neither proves the command did
not run. The CLI's manager commands, cotal invoke, the cotal run verbs and the manager row of
cotal status re-describe and re-issue an unpinned call after each such refusal, up to 16 times,
so a split reaches the operator only when every attempt split. A hosted run's own manager calls
use the same bound. A pinned call is never re-issued. An agent's own manager
tools, such as cotal_spawn and cotal_despawn, re-describe and re-issue with the same bound,
including the goal-result read that follows a spawn to its outcome.
An unpinned targeted call, such as cotal_despawn or a hosted run's turn relay, can also reach a
manager that does not host its target, because each manager resolves targets against the agents
it runs. That manager refuses with expired and not-executed and says it holds no mapping for
the target, and the same re-issue repairs it within the same bound. An agent that no manager hosts
still ends in that refusal once the re-issues run out. A pinned call gets the refusal of the
instance it named.
Manager availability
A verified Manager status reply includes host, the answering process's host name, and
version, the running @cotal-ai/manager package version captured when its module loaded.
These fields are advisory display metadata. They do not select a Manager, authorize a caller
or determine placement. Use the verified responder's instanceId for attribution. Clients
must accept status payloads that omit either field and leave the missing value unknown.
The optional fields advance the Manager cluster revision to 28.
A manager whose boot inventory marked every declared connector unavailable does not subscribe
spawn or launch on the class one rail. Those commands stay on scatter and on this
instance's inst rail, so a sibling that can launch them can take an unpinned spawn, and a
caller that pins this instance with --on still gets a named harness refusal. describe
still lists the commands: the instance rail serves them, and describe itself stays on the
class rail (SPEC 13.7). An unpinned spawn can therefore bind-fence: describe may land on
the skip member while spawn lands on a sibling, the command was not run, and the caller
re-issues or pins --on. status reports classSpawn: false when that skip is in effect.
When no manager takes the class queue, the broker answers an unpinned spawn with no
responders, and the caller classifies that not-executed (SPEC 13.3).
A manager that can launch some connectors keeps the class rail. If the queue hands it a
harness its inventory marked unavailable, the refusal names --on because the standing serve
credential cannot read sibling inventories. Pin the capable instance (the whole id, as ps
prints it).
Scatter liveness
ps and
status become a scatter across every registered instance: the caller freezes the
expected set from the service registry, invokes each under a shared deadline, and merges the
results with per-instance attribution. A non-answering instance is labelled as registered
with no answer within the deadline, never silently omitted. See SPEC §13.5 (scatter) and cli.md.
The expected set comes from the registry, which records registration rather than liveness. An instance that crashes never deregisters, so it stays in the set and the gather has nothing left to wait for but an answer that cannot come. It pays the whole deadline, on every scatter, indefinitely. A scatter can therefore be given a per-instance liveness probe: when the broker itself reports that an instance holds no subscription on its own instance rail, the gather stops waiting for it. Only that affirmative report counts. A lapsed presence entry, a probe that timed out, and a probe that failed are all absence of evidence, and treating any of them as death would turn a slow correct answer into a fast wrong one, so they leave the full deadline standing. Nothing about the outcome changes either way: an instance that did not answer is still unreachable, still surfaced, and the scatter is still not complete.
The probe is supplied by the caller, not invented by the scatter. Asking about an instance is
a publish on that instance's rail, and a credential that holds no row for it is refused by the
broker asynchronously, while the publish itself returns normally. The probe verb watches for that
refusal and raises it as permission-denied naming the rail, so it is never mistaken for a quiet
instance, and it never burns the probe budget waiting out a refusal. Only the layer that
minted the credential knows which ids it may ask about, so that layer asks about those and no
others. cotal ps freezes the class on its first connection, re-mints an instrument pinned only
to the frozen ids, and scatters on a second; a refusal the broker raises anyway is printed and
the instance's row says the probe was refused, which is a fact about the credential, not about
the instance.
This does not help against an instance that is connected but not answering. A hung manager holds its subscriptions, so it is indistinguishable from a slow one, and it still costs the full deadline. That is the correct result, not a gap in the probe.
Deregistration
A probe makes a dead registration cheap to skip; it does not remove it. Removal is the
registration's own exit, and there are two explicit routes to it
(SPEC §13.5: a deleted svc spec is the deregistration).
A manager that stops cleanly removes its own registration, so an ordinary shutdown leaves no stale row. The delete is pinned to the registration revision that process wrote. When a successor has registered the same instance since then, the stop logs that and leaves the successor's registration alone. It refuses that delete while this instance holds the endpoint governance slot at the live issuance-gate generation (a registration still completing its reopen). A leftover slot whose generation is behind that live generation is not in-flight and does not block the stop. A manager that cannot renew or read its lease keeps serving, stays registered, and retries. If another process holds the same instance key, that process has taken the instance over, so this one logs the conflict and exits without deregistering, leaving the successor's registration alone.
A restart that died mid-registration is a different residue: the issuance gate stays frozen under
that op. The successor completes the dead registration on boot when the freeze-holder is
affirmatively gone under a complete CONNZ sweep (the same composition as
cotal reconcile-gate). A committed spec write is finished under that
same freeze; only a definite no-commit abort-reopens and then runs the normal takeover.
Once that reopen commits, the registration has completed: a governance slot it then fails to release
is left behind the live generation, as on a normal registration, and does not fail the restart.
It does not invent a TTL and it does not start a new freeze over a still-held one.
A registration's own reopen fails in one of two ways, and its unavailable error says which. A
reopen that lost its compare-and-set wrote nothing: a reconciler or newer barrier moved the gate. A
reopen that threw, such as one whose acknowledgement was lost, has an unknown outcome and may have
committed. Either way, re-read the gate before retrying.
That residue has a second half, and it is the endpoint governance slot rather than the gate. Every registration takes the endpoint-wide slot before it publishes its spec and holds it until its own gate reopens, which is what serializes registration for the endpoint. An instance that stopped between those two points leaves the slot held with no registration behind it, so the endpoint refuses new registrations while nothing is actually in flight. The slot is stamped with the generation of the gate its holder had frozen when it took it, and a slot is promoted only at that same generation. So once the holder's gate has reopened past the stamp, the slot can never be promoted by anyone, and the next registration for that endpoint replaces it. That reclaim is part of an ordinary start and needs no operator step.
A slot whose holder's gate is still at the stamped generation is a registration that is genuinely in
flight, and it keeps refusing. The two states read differently only in the holder's gate coordinate,
so reopening that gate is what separates them: the holder's own restart heals it on boot, and
cotal reconcile-gate is the operator's route when the boot path cannot
run. The registration path is the slot's only writer, and neither repair command writes it.
A registration that cannot read the holder's gate at all refuses, because an unreadable gate does
not distinguish the two states either. Each of these refusals carries
kind = ai.cotal.ep.foreign-slot-held in error.details[] with the holder's instance id and the
condition that refused: in-flight for a holder gate still at the stamp, or no-seam,
unreadable, garbled or behind when the registration could not read that gate or read it below
the stamp. A remote manager asks its host to reconcile the holder only on in-flight, the one
condition a gate repair can clear.
For the instance that cannot cooperate, an operator names it:
cotal endpoint deregister <endpoint> <full-instance-id>
(cli.md). It removes the
record only on the same evidence cotal ps acts on: the broker reporting nothing subscribed on
that instance's own rail. It refuses if the instance answers a describe, refuses if the probe could
not run at all, and refuses if the instance is merely quiet, because a hung process still holds its
subscriptions and is therefore not affirmed gone. It also refuses while that instance holds the
endpoint governance slot at the live issuance-gate generation (a registration still completing);
a leftover slot behind that generation is not in-flight and does not block. Nothing sweeps the
registry on an age threshold or on silence.
An instance that is deregistered while it is merely wedged re-registers over the tombstone on its
next start, which is what makes the operator's decision a recoverable one.