Cotal
DocsAPI Reference
Agent coordinationReference

Workflow effect contracts

Personal native participants

A run started by an approved personal native conversation may bind that same session as a durable turn target:

const session = await participant({ name: "native-session" })
await turn(session, { name: "review", deadline: "30m" })

The program cannot name an actor or lifecycle. participant() reads the immutable run admission, pins the exact personal lifecycle in the journal, and refuses if that lifecycle is not live. Before starting the program, the conversation opts in with cotal_turn_participate. The manager issues a short lease for its self-only turn relay. The lease grants no stop, attach or terminal custody, and must be renewed after a reconnect or manager restart.

Spawn ownership

spawn is the manager's spawn action submitted under the step's own identity: the goal binds under the step's request id, so a resumed run re-attaches to the same seat instead of allocating a second one, a spawn whose seat fails to come up is catchable as L4002 with the manager's recorded reason (one the endpoint refuses at accept is L4000, described below), and a spawn on a race branch that loses is despawned by the run's own cancellation sweep. A seat belongs to the run that spawned it: when the run completes, it despawns every seat it spawned, including a race winner's and one whose spawn failed while its process stayed up. A spawn marked onFork: "adopt" is the exception: a fork can share that seat and no run can see whether another still uses it, so the seat stays up until you stop it with cotal stop once every run sharing it is done. A seat a migration handed to a later spawn follows that spawn's policy, and it stays up if any spawn that held it was marked onFork: "adopt", because a fork taken before the migration may still share it. In a space with several managers, a despawn counts a seat as already gone only when the manager that allocated it says so. A run that fails or is released keeps its seats until a resume completes it or you stop them with cotal stop. Start a seat with cotal spawn when it should outlive any run.

Seat budgets

permits are the budgets this host meters: turns, how many turns the run may dispatch to the agent, and wallClock, a duration from the spawn after which no turn is admitted. The turn that would exceed one is the catchable L4001 (kind permit-turns or permit-wall-clock; a deadline the remaining wall clock cannot hold counts as exceeding it), an adopted run counts the turns its journal recorded, and a budget the host has no meter for, such as tokens or spend, is refused at the spawn rather than accepted and ignored.

Supervision

supervise is the restart policy this host asks the manager to enforce: restarts, how many in-window process deaths may come back under the same handle, and window, the duration those deaths are counted in (default 10m). The manager restarts the process in place under the same name, lifecycle uid, persona, worktree and permits; monitor does not fire for a restart, and wait(down) fires only when the seat is gone for good. Spending the budget retires the seat, and the next turn is the catchable L4002. The policy also covers the manager's own death. On a static mesh, when the manager ends without stopping the seat (a crash, or a pty seat that ended with its manager), the next manager of the same instance restarts the seat the same way and counts that as one restart. A turn the dead manager had accepted for the seat is served to the restarted seat, so the interrupted turn runs again and finished turns do not. A turn an older manager accepted is not served again, because its record does not name the seat's owner, and it ends at its deadline as L4003. A seat whose budget is spent, or whose persona file or credential changed or expired while no manager ran, is retired instead. A policy this host cannot enforce (an unknown key, a user-mode seat, or a runtime that cannot respawn a name in place) is refused at the spawn rather than accepted and ignored.

Event plane

events: false is the workflow form of cotal spawn --no-events: the seat starts without its AG-UI event plane. A connector that publishes none, such as Hermes, needs it, because an omitted events arms the plane and the manager refuses that connector at the spawn. A value that is not a boolean is refused at the spawn. runtime names the terminal runtime of this one seat (pty, or an installed extension such as tmux). Omitted, the seat uses the serving manager's own runtime. A name that manager cannot resolve refuses the spawn and starts nothing, and the other seats keep the runtime they already have.

Conclave membership

conclave joins its members to a real channel as durable membership rows: the channel derives from the step's own request id when the program names none (a program-named channel is borrowed, never torn down, and a membership that predates the conclave survives its close), each member handle resolves to its principal through the seat's own presence row (an absent member is catchable as L4002), each member's row is written under that lifecycle's exact read ACL (read just before the write and again after it, so a member retiring meanwhile gets no row and keeps none), and a conclave cancelled on a losing branch is released by the same cancellation sweep.

Ask attempts

ask parks one checkpoint-plane pause per attempt, answered through cotal run answer as a checkpoint is, and tells the agent through the same relay turn uses: one relay per attempt under the attempt's own token, carrying the schema, the attempt count, the deadline and the previous refusal, which the seat's connector renders as the record wanted and the native command that answers it. The relay names the ask's absolute deadline, so the manager stops serving an attempt at the instant its pause expires and refuses a relay already past it. The seat calls cotal_describe for endpoint manager, then cotal_invoke with endpoint: "manager", command: "run-answer", self: true and args containing the relayed runId, stepKey and requested value. This uses the native connection's own identity even when the harness strips the shell's mesh environment. It needs no run grant or run-status read. Answer before ending the host turn. cotal_yield submits no answer value and can withdraw the pending relay that authorizes a baseline answer. The manager records the authenticated caller as the answerer. A spawned seat's baseline credential carries only the self-targeted run-answer row, and the manager accepts it only for the open ask or escalation relayed to that exact incarnation. It cannot answer another seat's ask, an unrelayed checkpoint, another run, or start and resume commands. An ask addresses an agent the run spawned (anything else refuses before an attempt opens), a resumed attempt tells the seat nothing twice, and a seat gone at the relay is L4002. On the pause itself: the shorthand of the language reference §6.5 is enforced (an unreadable schema is L4022), a non-conforming answer costs one attempt and its refusal reason is recorded on the entry for the answerer to read, exhausted attempts (default one) are the catchable L4006, and so is the one absolute deadline for the whole ask passing with no conforming record (its kind is ask-deadline), including one that passes before an attempt's pause is armed.

Checkpoint journal entries

checkpoint binds what it asks on its own entry, so cotal run show prints the question under the step key an answer is addressed by while the pause is open: the address alone left whoever was asked reading the source to find out what "approve" meant. The entry also records the open attempt's deadline and the onExpiry it was armed with, which run show --json reports; what an expiry does is still decided from the program's source. An escalation's attempt records its own deadline, one timeout after it opens, and a resumed attempt keeps the deadline it recorded. After a checkpoint or ask accepts an answer, the journal prints that accepted answer's recorded value and attribution under the settled step. A checkpoint's comes from its frozen result, and the journal never substitutes another filed answer or invents fields that result does not hold. An ask's result is the value alone, so its line is read from the answer record its last attempt's settle named, even when that value is a record with fields named like a checkpoint's. Amendments print on their own lines after it.

Escalation

An escalate addressed to an agent this run spawned is relayed to that seat through the same turn relay an ask uses, carrying the prompt and the token to answer under, held to the pause's own deadline; a to naming anyone else is a person, and their pause stays the one anybody can answer, with the addressee recorded and rendered beside the question.

Presence liveness

monitor registers interest in an agent, and the registration is the journal entry itself, carrying the handle it registered: monitoring an agent that is already dead succeeds, and the death is the wait's to observe. wait(down(...)) observes a monitored agent, and refuses one the run never performed monitor on. It reads the death off presence liveness, the same witness a conclave join resolves members through: the value carries the handle, the reason (lapsed when nothing live holds the name any more, superseded when a live row holds it under a different incarnation) and the time of observation. Presence liveness skips any value without a string card.id equal to its key, a string card.name, a known status and a finite ts, so a participant that publishes a malformed row under its own key cannot fail another run's wait, turn or conclave join. A superseded incarnation is down at once. A lapsed one is down only after its presence row has stayed gone for 30 seconds, because a seat whose connector stalls past the row's 6-second TTL, under host load or across a reconnect, renews it under the same incarnation and is still working. The 30 seconds count only across presence reads that each end within 6 seconds of the previous one starting, so a slow read or a run of failed reads, which could hide a renewal, starts the count over. Even then the run asks the manager first: a seat whose process the manager still runs is alive however long its row has been gone, so the count starts over. Only the manager that spawned the seat can report it gone, because another manager in the space knows only its own seats, so the run asks again until that one answers. When it does not answer, the presence row is the only witness and the lapse stands. The asking stops at the wait's timeout or cancellation, and at a turn's deadline, yield or cancellation, which then decide. A wait that begins after the death resolves once that holds, and a timeout resolves null on one absolute deadline a resumed run re-attaches to.

Turn relay

turn wakes one seat for one host turn through the manager: the run submits the turn under the step's own identity, the manager holds it as a goal pinned to the seat's incarnation, and the seat pulls it under its own reach ahead of its next host turn, so nothing is pushed into a session mid-thought. The manager sends the seat a turn notice whenever its pending turns change, and repeats it every 5 seconds until the seat pulls a new turn. The seat pulls when a notice arrives, when it first connects, and after a reconnect while it holds a turn, so an idle seat sends no requests (SPEC §14.3). The payload the seat reads names the run and the step and carries the rendered run context, plus any pending notices addressed to it, which the turn consumes. The seat yields through cotal_yield (done, blocked, or handoff with an addressee), and ending its host turn yields done for every turn it was shown. A cotal_status call changes presence only, including when it sets idle. It does not yield a turn. If a yield times out with an unknown outcome, it may already be recorded. After reconnecting, retry the same turn id and intended status. An already-settled reply confirms a terminal turn, not which attempt settled it. Success records a still-active turn, so this retry is not a read-only check. A handoff names another seat the same run spawned: the next turn in the same scope to that seat records the link, a handoff to a name the run never spawned is the catchable L4005, and one to a seat bound to a different worktree is L4004. The deadline elapsing before any yield is the catchable L4003: the acceptance names the instant, the manager's goal-bound hold denies at it, and the run arms its own pause on that same instant, so either side outliving the other still converges on the same answer. A seat that pulled the turn earlier stops showing it to its session at that same instant. A seat that dies mid-turn is read off its own presence row by the run itself, with the same 30-second confirmation for a lapsed row, and is the catchable L4002, and a death the manager marked on the deadline terminal reads the same way. Two turns on one seat, from two branches or from two runs, reach it one at a time: the language dispatches the second when the first settles, and the manager shows a seat the oldest unsettled turn alone. A turn's goal id belongs to the caller that submitted it, so two callers may each submit a turn under the same id. A seat yields by goal id alone, so the manager refuses to hand one seat a second caller's turn under an id it already holds. On an auth mesh the relay needs no extra grant: every spawned seat's baseline credential carries its own pull, yield, and caller-bound answer rows, the run driver's operator instrument carries the turn request and the inspect read that confirms a lapse, and the manager arms the deadline hold over its own serve grant and expires it itself once due. An accept the manager cannot finish is unwound to a failed terminal on the goal it bound, and a retry of that submission is refused naming the terminal rather than accepted a second time.

Reply waits

wait(replied(...)) observes those turns from another branch: a completed turn is a reply, and the wait resolves with the observation record (the handle, the yield's status and note, the yield's own stamp). It reads as a level, the way wait(down) does: a reply that already exists resolves the wait at once, and two replies resolve to the latest by the yield's stamp. A denied or cancelled turn is never a reply, so an unanswered wait rides its own mediated timeout to null, and a handle the run never spawned or turned refuses loudly, since only this run's turns are observable. A turn the run itself ended without an accepted yield (its deadline, a cancellation, a refused handoff) is never a reply, whatever the seat yields to the relay later.

Cancellation

A cancelled branch also withdraws what it relayed to a seat before its cancellation completes: its turn, its ask attempt or its escalation ends cancelled through the manager's reserved cancel (SPEC §13.6), so the seat is not shown it after the branch's scope settles and the next turn to that seat does not wait behind it. Only the manager that accepted the relay holds it, so in a space with more than one manager the run sends the cancel again while another manager refuses it, until the accepting one answers. A cancel refused as unavailable, such as by a manager still reconciling at boot, or one whose reply is lost (deadline-exceeded) is sent again until it lands or the relay's deadline passes, so the branch's cancellation does not complete while the seat can still be shown the relay. Any other refusal fails the branch's step at once under its code, since sending it again changes nothing: a manager that serves no cancel is refused not-found before anything is sent, and an accepting manager with no such relay pending refuses failed-precondition. The run reads the relay's deadline from its goal and retries a failed read, since the relay may still be served until the deadline is known. A goal record that can never yield a deadline, such as one that is not JSON, fails the branch's step at once, and so does a refusal that lasts past the deadline.

Placement

A spawn may bind its agent to a logical worktree (spawn("builder", { worktree: "wt-1" })): the handle carries the id, and the run enforces the one rule the language states about it: two agents never share a worktree concurrently. The validator rejects the literal case up front (L3022: two branches of one concurrent scope spawning into one literal worktree, named branch functions included), and the runtime guards the rest, computed ids included: a spawn claims its tree before it submits, so a second spawn into a tree held by a live seat or by a spawn still bringing one up is the catchable L4008, a spawn that ends without a handle gives the tree back, and the tree is reusable the moment a holder's presence row is gone, so a discharged race loser or a crashed seat releases its tree with no bookkeeping. A spawn the endpoint refuses at accept is the catchable L4000 (L4001 when the refusal is the endpoint's seat capacity), and one whose seat never came up is L4002. A refusal that states the command did not run is answered before it gets that far. In a space served by more than one manager the resolve and the invoke are separate trips through the same anycast queue, so a run's call can reach an instance it did not resolve against, and that instance refuses ahead of any effect. The run drops its resolved handle, re-describes and re-issues, for a bounded number of attempts; after them the refusal surfaces as the effect's own failure and still states that nothing ran. A spawn that names a placement addresses one instance by name, so a refusal from it is that incarnation answering about itself and is never re-issued. When the spawn also names cwd, that manager resolves the existing absolute directory to its canonical host path before accepting the spawn. A missing, relative or non-directory path refuses with no seat and never falls back to the manager workspace. One hosted run may name one placement instance; a program naming several is refused instead of widening one run credential across hosts. Placement is accepted only from an object-literal spawn option bag whose endpoint and instanceId are string literals. A computed option bag, a computed placement field, or a spread in either object refuses at run-start. The option argument may be absent, and an object-literal bag without placement is accepted when it names no cwd. A spawn that names cwd must also name placement, because that directory belongs to one host. Without placement it refuses with L4000 before submitting a spawn. Read the manager instance with cotal ps, then put its full instance ID into the program as a literal:

const worker = await spawn("worker", {
  placement: { endpoint: "manager", instanceId: "abcdefghijklmnopqrstuvwxyz" },
  cwd: "/home/operator/project"
})

Replace both example values with the manager instance and an existing absolute directory on that manager's host. No manager is chosen by class anycast for this spawn.

Recovery cleanup

A turn handoff across worktrees is the L4004 described above. Recovery keeps these honest: a resumed run reseeds its roster, holders and handoff memos from its own journal, and the driver re-issues any recorded-but-undischarged cancellation at adoption, before the engine performs a new step, so a loser a crash left alive does not keep its seat or its tree while the resumed run works on. The same sweep withdraws a cancelled branch's relays and undelivered notices: a notice waits on the run for its addressee's next turn, so a decision the run cancelled would otherwise arrive at an agent with nothing to distinguish it from one that stood.

On this page