@cotal-ai/pi 0.92.0 → 0.92.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +12 -12
- package/dist/standalone.js +19 -17
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -6571,9 +6571,9 @@ var require_ajv = __commonJS({
|
|
|
6571
6571
|
}
|
|
6572
6572
|
});
|
|
6573
6573
|
|
|
6574
|
-
// ../../node_modules/.pnpm/ajv-formats@3.0.
|
|
6574
|
+
// ../../node_modules/.pnpm/ajv-formats@3.0.1/node_modules/ajv-formats/dist/formats.js
|
|
6575
6575
|
var require_formats = __commonJS({
|
|
6576
|
-
"../../node_modules/.pnpm/ajv-formats@3.0.
|
|
6576
|
+
"../../node_modules/.pnpm/ajv-formats@3.0.1/node_modules/ajv-formats/dist/formats.js"(exports) {
|
|
6577
6577
|
"use strict";
|
|
6578
6578
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6579
6579
|
exports.formatNames = exports.fastFormats = exports.fullFormats = void 0;
|
|
@@ -6774,9 +6774,9 @@ var require_formats = __commonJS({
|
|
|
6774
6774
|
}
|
|
6775
6775
|
});
|
|
6776
6776
|
|
|
6777
|
-
// ../../node_modules/.pnpm/ajv-formats@3.0.
|
|
6777
|
+
// ../../node_modules/.pnpm/ajv-formats@3.0.1/node_modules/ajv-formats/dist/limit.js
|
|
6778
6778
|
var require_limit = __commonJS({
|
|
6779
|
-
"../../node_modules/.pnpm/ajv-formats@3.0.
|
|
6779
|
+
"../../node_modules/.pnpm/ajv-formats@3.0.1/node_modules/ajv-formats/dist/limit.js"(exports) {
|
|
6780
6780
|
"use strict";
|
|
6781
6781
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6782
6782
|
exports.formatLimitDefinition = void 0;
|
|
@@ -6846,9 +6846,9 @@ var require_limit = __commonJS({
|
|
|
6846
6846
|
}
|
|
6847
6847
|
});
|
|
6848
6848
|
|
|
6849
|
-
// ../../node_modules/.pnpm/ajv-formats@3.0.
|
|
6849
|
+
// ../../node_modules/.pnpm/ajv-formats@3.0.1/node_modules/ajv-formats/dist/index.js
|
|
6850
6850
|
var require_dist = __commonJS({
|
|
6851
|
-
"../../node_modules/.pnpm/ajv-formats@3.0.
|
|
6851
|
+
"../../node_modules/.pnpm/ajv-formats@3.0.1/node_modules/ajv-formats/dist/index.js"(exports, module) {
|
|
6852
6852
|
"use strict";
|
|
6853
6853
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6854
6854
|
var formats_1 = require_formats();
|
|
@@ -27014,10 +27014,10 @@ import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAll
|
|
|
27014
27014
|
import { parsePrincipalKey as parsePrincipalKey2, peerLabel as peerLabel3 } from "@cotal-ai/core";
|
|
27015
27015
|
|
|
27016
27016
|
// ../connector-core/dist/docs-bundle.generated.js
|
|
27017
|
-
var DOCS_VERSION = "0.92.
|
|
27017
|
+
var DOCS_VERSION = "0.92.1";
|
|
27018
27018
|
function loadDocsBundle() {
|
|
27019
27019
|
return {
|
|
27020
|
-
"version": "0.92.
|
|
27020
|
+
"version": "0.92.1",
|
|
27021
27021
|
"generatedFrom": "docs/*.md + SPEC.md + spec/cotal-lang.md + spec/cotal.schema.json",
|
|
27022
27022
|
"pages": [
|
|
27023
27023
|
{
|
|
@@ -27102,7 +27102,7 @@ function loadDocsBundle() {
|
|
|
27102
27102
|
"title": "Connect Claude",
|
|
27103
27103
|
"kind": "Guide (informative)",
|
|
27104
27104
|
"summary": "The Claude Code connector turns a real claude session into a Cotal mesh peer.",
|
|
27105
|
-
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Its lifecycle hook imports the relay from the\n`@cotal-ai/connector-core/relay` subpath, so each hook process loads the relay and its environment\nreaders and none of the NATS client, zod or yaml. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Hermes](connect-hermes.md) (alpha), [pi](connect-pi.md) (alpha); the\n[Connectors](connectors.md) matrix compares them feature-by-feature.\n\n## Set up\n\n> **Team and Enterprise accounts:** [Enable Claude Code channels](#enable-channels-for-your-organization)\n> before running unattended agents. Channels are blocked by default, so incoming messages cannot\n> wake an idle session until an organization Owner enables them.\n\n```bash\ncotal setup --configure-only # install the plugin and seed one agent without startup\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin for your user account, so every Claude Code session on\nthis machine gets the `cotal_*` tools, in any project. That covers the terminal, the IDE extensions\nand the Desktop app's local Code sessions. Setup can use Desktop's bundled Code executable when no\nterminal `claude` command is installed. Desktop-only users should open a local Code session once so\nDesktop downloads that executable. Setup checks it through the connector's own readiness probe;\nmanaged `spawn` still requires the terminal `claude` command. With a local mesh already running, run setup once, reopen Claude\nand ask it to join your local mesh. No MCP configuration edits or special launch flags are\nneeded for this native plugin path. It also shares your own MCP servers with spawned sessions on its first run (see\n[Sharing your MCP servers](#sharing-your-mcp-servers)), and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nA repeat `cotal setup` and `cotal update` refresh the copy under `~/.cotal/claude-plugin` and update\nexisting Claude plugin registrations, including older cached installations. After an npm upgrade,\nthe first regular Cotal command also refreshes these copies before accepting the new generation. Setup,\nskills installation and refresh share a machine-local lock through the final plugin check, so simultaneous\ncommands cannot overwrite a generation while another command verifies it. Disabled installations\nstay disabled during an update. `cotal status` checks the resolved payload and its recorded version,\nso a current marketplace cannot hide an outdated or incomplete plugin. Running sessions keep the\ncode they loaded; reload plugins or reopen Claude to load the update.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide). The Claude connector declares and implements this\n setup provider, including the marketplace assets and native plugin commands; the base CLI only passes\n the vendor-neutral Agent Skills directory. The plugin carries no code and no core dependency,\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release. `cotal update` refreshes an existing skills plugin without\n enabling it or installing a removed one; `cotal setup --skills` also installs missing skills. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind. The same provider reports the plugin and skills plugin rows\n in `cotal status`, which point a stale or missing skills plugin at `cotal setup --skills`.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it\n (and `cotal setup --skills` does only that):\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile, and names `cotal setup --skills` as the remedy. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Enable channels for your organization\n\nFor claude.ai Team and Enterprise organizations, channels are blocked by default. An organization\nOwner must enable them in [Organization settings > Claude Code > Channels](https://claude.ai/admin-settings/claude-code),\nor set this organization-managed setting:\n\n```json\n{\n \"channelsEnabled\": true\n}\n```\n\nUsers cannot override this setting through local or project settings. Cotal's\n`--dangerously-load-development-channels` launch flag cannot override it either. Enable channels\nbefore launching the sessions that need them.\n\nWhen channels are blocked, Cotal's MCP tools and hook-based inbox delivery still work when the agent\ntakes a turn. An incoming message cannot wake an idle Claude Code session. A successful spawn or a\nconnected MCP server does not prove idle wake is available; check Claude's startup notice before\nleaving agents unattended.\n\nPersonal Pro and Max accounts without an organization do not need this admin opt-in. Sessions still\nneed to opt into their channel, which Cotal's managed launch does for its own server. See\n[Anthropic's channel controls](https://code.claude.com/docs/en/channels#enterprise-controls) for\norganization policy and other authentication modes.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona` / `cotal_personas`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches its declared capabilities. `cotal_run` is gated separately by\n`run`; use `capabilities: [spawn, run]` for both. Fresh setup defaults include both.\nSee [workflow tool setup](workflows.md#from-an-agent-session) for a first run and missing-tool checks.\nClearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | `http` lifecycle hooks \u2192 POST to the connector (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal \\\n --permission-mode bypassPermissions\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Tool permissions.** A managed seat has no human at its terminal, so it launches with\n `--permission-mode bypassPermissions`: a prompt under the default mode would stop its first mesh\n tool call until someone answered it, and nobody can. To get prompts back, pass\n `--opt permission-mode=default` (or another mode); the connector adds the default only when the\n launch options carry neither `permission-mode` nor `dangerously-skip-permissions`. Claude refuses\n the bypass mode as root and under a policy that disables it; such a host must pass a mode.\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set:\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **Persona privacy.** The persona body is written to a private file and Claude receives only\n `--append-system-prompt-file <path>`. The body never appears in the spawned process argv. The\n carrier is a 0600 file inside a 0700 directory on POSIX, with equivalent owner-only ACL hardening\n on Windows. That is OS-user isolation: any process running as your user can read it while it\n exists. The manager or the foreground `cotal spawn` removes it, and the shared-server MCP config\n file, once it has proved the `claude` process gone. If the launcher is killed first, a watcher\n started beside `claude` removes them when `claude` exits.\n- **MCP servers.** A spawned agent loads Claude's native MCP sources alongside Cotal and declared\n sharing overrides. Explicit `--share-tools` selections and carried private homes keep strict\n isolation. Claude still applies its own server approvals, disabled settings and managed policy\n (see below).\n- **Lean MCP server.** Every session runs its own cotal MCP server, so Node starts it with\n `--optimize-for-size --max-semi-space-size=1` to shrink its memory footprint. Managed launches and\n the plugin's `.mcp.json` (personal plugin sessions) both pass these flags. `--lite-mode` and\n `--jitless` would save more, but they remove `WebAssembly`, which Node's built-in `fetch` needs,\n so WebAssembly stays enabled.\n- **Light hook.** Claude Code starts the plugin's hook as a new process for every tool call, prompt\n and stop. The hook imports `@cotal-ai/core` only through small subpaths such as\n `@cotal-ai/core/peer-label`, never the package root, which would bundle YAML and NATS into it.\n `pnpm smoke:claude-hook-weight` holds that.\n- **Installed plugin.** The plugin is installed once, at user scope (`claude plugin install\n cotal@cotal-mesh --scope user`), because its hooks bind only to an *installed* plugin. Earlier\n releases installed it per project, at local scope; the next `cotal setup` removes those installs and\n keeps the user-scope one. If Claude Code cannot change a project's settings, setup still removes\n the others, then fails naming that project and the `claude plugin uninstall` command to run there. A session Cotal launches loads the package's own copy with `--plugin-dir`,\n which replaces the installed one for that session, so it still runs one cotal server.\n The repo's `.claude-plugin/marketplace.json` lists the committed plugin tree under\n `claude-plugin/`, which each release regenerates with the built bundles, the skills and the\n release version, so an install from the repo or from a pinned commit runs without a build\n ([Release](release.md)). `cotal setup` (npx, no clone) materializes the same marketplace under\n `~/.cotal/claude-plugin/` from the installed CLI (each plugin dir is rebuilt from scratch and\n atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its manifest and install behavior ship inside the Claude connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** A launched session joins under the identity in `COTAL_NAME`, `COTAL_LINK`\n or `COTAL_AGENT_FILE`. A plain `claude` with none of them joins only when its conversation calls\n `cotal_connect`, so your own sessions do not appear as stray peers. Until then its MCP\n server builds no mesh agent and opens no broker connection. See\n [Sessions Cotal did not launch](#sessions-cotal-did-not-launch).\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt. The runtime waits for the\n dialog title in normalized terminal text and presses Enter once when it appears, so startup speed\n does not affect a supervised launch. Claude skips that prompt when channels are off for the\n session, for example under a Team or Enterprise organization whose managed settings do not set\n `channelsEnabled: true`, and opens its input screen instead. The runtime then ends its watch\n without a key once the input screen's footer shows `(shift+tab to cycle)` or `? for shortcuts` on\n two screen reads in a row. That seat gets mesh messages through its hooks only, so a message does\n not wake it while it is idle. The PTY runtimes read the child's screen, and the tmux, cmux, Orca\n and Herdr runtimes read the pane's screen. If neither the prompt nor the input screen appears\n within 15 seconds, the seat ends with a bounded error naming the unmatched prompt instead of\n hanging silently or answering another dialog. The PTY runtime writes that error to the seat's output and\n stops the child as a graceful stop does, so a child that ignores SIGTERM is killed 3 seconds\n later. The other runtimes write the error to the manager's log.\n- **Trusted directory.** An authorized supervised spawn accepts the selected workspace through\n Claude's native trust dialog, which Claude draws in one of two layouts. When a numbered list\n shows Yes first and selected, the runtime confirms it with Enter. When No comes first and is\n selected, the runtime moves to Yes and confirms only after seeing Yes selected. It never confirms\n while No is selected, sends each response once, and stops watching after the\n development-channel confirmation or on the input screen. A changed or unsupported dialog receives no guessed input\n and still fails within the startup bound. Claude saves trust with its own repository and linked\n worktree rules; Cotal does not rewrite the manager's configuration. Accepting trust enables the\n workspace's hooks and settings, which can select a tool permission mode when no launch flag\n overrides it. Cotal adds no tool-permission bypass flag. Review the checkout before authorizing\n a spawn there. A foreground `cotal spawn` keeps the trust dialog in your terminal.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\nThe consuming connector holds a crash-safe claim for its broker target, space, principal and\nlifecycle on this OS user account. A second patched session on the same host refuses before\npulling that inbox, even when its launch files and control socket differ. Reconnect keeps the\nclaim. Hooks and non-consuming helper connections do not claim it. The control listener binds\nbefore the broker connection and never removes an existing socket path.\n\nThis is cooperative same-host containment. Other hosts, broker URL aliases and older clients\nare not fenced. Preserve the existing session before choosing a distinct actor for a new\nlaunch. An uncertain shutdown keeps custody until the consuming process exits.\n\n## Sessions Cotal did not launch\n\nA Claude Code session you start yourself, where the `cotal` plugin is installed, gets the tools the\nhosted connector offers: `cotal_connect`, `cotal_wait`, `cotal_disconnect` and the routed mesh tools.\nNothing joins until the conversation calls `cotal_connect` with a name. The plugin's server then runs\n`cotal app-identity provision` with the CLI that `cotal setup` recorded beside the plugin\n(`dist/cli.json`), so it does not need `cotal` on `PATH`. Each conversation gets an identity of its\nown on the mesh a bare `cotal spawn` from the session's directory would pick. A name that a live peer\nholds moves to `<name>_2`, `<name>_3` and so on. The identity's channels and role come from the mesh's\ndefault persona. `cotal_disconnect`, the end of the session or a signal retires it, and the next\nprovision retires one whose server was killed. A plugin that `cotal setup` did not install has no\nrecord, and `cotal_connect` names that remedy.\n\nThe gateway's MCP annotations distinguish read-only queries from updates. Orientation, connection\nstatus, the roster and channel information are read-only. Sending messages, connecting and joining\nchannels are additive. Inbox reads and waits consume messages; status, channel mode, leaving and\ndisconnecting can replace or remove state. These hints do not grant access or override Claude's\npermission settings or an organization's approval policy.\n\nA message for a connected conversation starts a turn through the plugin's\n[native hooks module](https://code.claude.com/docs/en/plugins/mods/overview). This works in supported\nClaude Code hosts, including local sessions in the Desktop app's Code tab. The module runs a local\nwake reader as a child of the same host that runs the MCP server. Each server writes connection ids\nto its own socket in a private directory. The module submits the fixed notification\n`Cotal: a new message for connection_id <id>. Read it with cotal_inbox.` with Claude's plugin-sender\nattribution. It never submits it as the person's own words. While Claude is busy, the module appends\nthe notification at the next main-conversation tool or model-request boundary instead of queuing a\nsecond turn after the work ends. Peer message contents stay in the inbox. Between turns, the\ntranscript shows a compact **Cotal \xB7 new message** indicator; click it to inspect the full\nnotification. This changes only the display, not the message Claude receives.\n\nA wake follows the rule `cotal_wait` follows. It comes once until the conversation reads its messages,\nand not while a `cotal_wait` holds the connection. A replacement reader reoffers unread work.\nClaude's Stop and organization controls still apply. A refused wake stays pending without retrying\non every new message. `cotal_connect` reports whether the incoming reader is attached; attachment\nalone does not prove that the host admitted a turn. When native modules are unavailable or blocked,\nthe conversation can listen with `cotal_wait` instead.\n\nClaude Desktop chat does not run a plugin's local server. On macOS and Windows, where Claude Desktop\nis installed, `cotal setup` adds a `cotal` server to Desktop's own config,\n`claude_desktop_config.json` (Settings > Developer > Edit Config), and keeps every other entry there.\nIt runs the same server with `--desktop`, by the absolute paths of the node that runs the CLI and of\nthe plugin copy under `~/.cotal/claude-plugin`. Quit and reopen Claude Desktop to load it. A repeat\n`cotal setup` and `cotal update` keep the entry current, `cotal status` reports it, and a config\nfile that is not valid JSON is refused and left as it was. Setup can use Desktop's bundled Code\nexecutable when there is no standalone `claude` CLI. Its installed MCP and command hooks use the\nrecorded Node executable, so the app need not find Node on `PATH`. On Linux, setup writes nothing\nfor Desktop Chat.\n\nDesktop's working directory is not your project, so its mesh is the `cotal use` mesh, else the only\none running. Desktop runs no monitor, so a Desktop conversation hears messages when it calls a tool,\nand keeps calling `cotal_wait` when you ask it to keep listening. A chat with no turn running cannot\nbe woken by any server: Claude's chat surfaces start a turn only from the person.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n A hook whose handler throws still returns an empty reply so the session is never blocked, and\n that reply carries nothing, so it commits nothing: the batch it had started to surface stays\n un-acked and goes out on a later frame. The seat also drops any `turn-pending` row that breaks\n the manager contract, such as one with no integer deadline, and says so once in its log. A reply\n with no `turns` array changes nothing: the seat keeps the turns it already holds.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. When the channel becomes active, the connector\n first re-fires a focus mention remembered during startup, otherwise one buffered wake. A rejected\n push keeps its bounded retry, and JetStream redelivery remains the durable backstop for unacked\n inbox items. Neither a redelivery nor that retry repeats a nudge already pushed for that message,\n whether the message had its own nudge or was counted in a batch one, so a session held in a long\n tool call gets one nudge per message. Once a hook frame carries the message, or the push that\n announced it fails, its next redelivery nudges again, so a reply that never reached Claude Code\n still recovers. If the channel cannot run at all, delivery still waits for the next hook. Live-only\n traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is delivered in parts: once no smaller mail is waiting,\neach call carries its next part, and it is cleared only after its last part goes out, because clearing\nwhat was not handed over is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items first, then other\nchannel traffic, and a direct message or role request only when the whole buffer is directed mail.\nIt never evicts a message the connector is handing to the session or a turn is still carrying.\nAn evicted channel item is acknowledged. An evicted direct message or role request never is, and\nthe broker redelivers it after the ack wait until a redelivery finds room. A direct message stays\npending on the session's DM durable, where `cotal deliver pending <name>` counts it. A role request\nstays on its role's shared queue, which that command does not read. A full inbox therefore delays\ndirected mail until the session drains it.\nIf the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content. Recall cannot tell one message with an\nempty id from an identical one with another disposition, so in focus such a message is held in the\nlocal inbox as pull-only instead of being dropped, and a mention of it still wakes the agent. When\nthe session settles an id-less copy, it reads the chat stream's last sequence. Identical copies\narrive in stream order, and those reads can answer out of order, so the first read that can see the\ncopies binds them in arrival order, latest first, each to the latest unbound stream copy at or below\nthe lowest sequence read for it or any later identical copy.\nWhile the connection stays up, every stream copy at or below that sequence reached the session\nfirst, so a later identical copy sent during a reconnect gap is above it and stays unbound. A copy\nthat arrives while a read runs may not be in that read, so it binds nothing there and recall reads\nthe channel again, up to three times; if it still could be a stream copy in the last read, recall\nleaves that stream copy in the stream and reports the channel as incomplete. A settled copy a\ncomplete read cannot bind is behind the focus start or out of retention, and is forgotten. Recall\nhands back into the inbox only the stream copies nothing is bound to, such as one sent during a\nreconnect gap or one the inbox evicted on overflow, and `cotal_inbox` hands each over once. Overflow\nfrees only the evicted copy, held or quiet, and a copy no read has bound yet keeps its place in\narrival order, so an identical muted copy stays out of recall. When\nthe inbox is full, recall leaves them in the stream for a later call and reports the channel as\nincomplete. A history read that fails, or a channel with replay off, settles nothing and is reported\nas incomplete, and recall calls run one at a time. If the sequence read for a settled copy fails,\nor answers only after the connection dropped, recall skips that channel for the rest of the focus\nperiod and reports it as incomplete. Recall cannot tell a late copy of a message it handed back from\na new identical message, so every copy takes its own disposition: a new identical quiet mention is\nstill delivered automatically, and a late copy can surface a second time.\nA recalled message that already went out in part is read to its last part, even if an exclusion\nlands after its first part. One session reads its inbox one call at a time: a `cotal_inbox` call\nthat overlaps another waits for it to finish, so neither decides from a view the other has already\nmoved past.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\nWhatever does reach a turn is framed so a peer cannot write the frame. A line that begins at column\nzero is written by the connector; one message is one line plus indented continuations, with the\nsender inside a single bracket pair. A message body, a sender name and role, and a service or\nchannel label are all peer-controlled, so each passes through the same neutralization the\n`cotal_inbox` reply uses: no line break a splitter may honour and no bracket survives into a\nrendered attribution. This matters more for an injected block than for a reply, because the agent\ndid not ask for it and so never had the chance to distrust it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\nA `SessionStart` during an open turn, including compaction, preserves the current `working` or\n`waiting` status until `Stop`, `StopFailure`, or `SessionEnd` closes the turn.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` only when no turn is open (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `PostToolBatch` | `working` when the main agent's batch resolves after a prompt left the seat `waiting`, which clears its condition so the turn's `Stop` yields; a subagent's batch (it carries `agent_id`) or one relayed after the turn's terminal changes nothing |\n| `Notification` (`permission_prompt` / `agent_needs_input`) | `waiting` with condition `approval` / `input` (activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). `StopFailure` also relays Claude Code's native error value as `condition.source` and maps it to the closed condition vocabulary. On the [event plane](#event-plane) it closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nThe connector also leaves gracefully when its stdin closes. An MCP client closes it to end the\nsession, and a killed `claude` closes it with no `SessionEnd`, so a dead session drops off the\nroster instead of staying on it as a live peer.\n\n`StopFailure` maps `rate_limit` and `overloaded` directly; auth and credential failures to\n`auth`; account and billing failures to `billing`; `invalid_request` to `request`;\n`model_not_found` to `model`; `server_error` to `server`; `max_output_tokens` to `context`; and\n`unknown` to `failed`. The native value remains in `condition.source`.\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. Claude Code's own\n[hooks reference](https://code.claude.com/docs/en/hooks) lists every hook event. The hooks the\nplugin registers, with their matchers, are listed in the\n[adapter README](../extensions/connector-claude-code/README.md#claude-code-hooks).\n\n## Event plane\n\nA spawned session publishes a **structured** account of what it\ndid: run boundaries per turn, assistant text, reasoning, and each tool call with its start\nand its end. Not prose about the work, the work itself, in a vocabulary a program can\nread. The launcher sets `COTAL_EVENTS` by default; pass `--no-events` to opt out on an unrestricted\nspace. A user-auth registration with `policy: { events: \"required\" }` carries `eventsRequired` in the\nprivate launch material, so the connector arms even without `COTAL_EVENTS`; `--no-events` is refused.\nA hand-driven user-mode session may carry the same decision as `COTAL_EVENTS_REQUIRED=1`. Its own\npublish grant must cover `events.<owner>.<actor>` or the connector refuses before joining. An unmanaged\nsession with no launch material and no required-policy fallback keeps the generic default behavior.\n\nIf the event plane stops, the seat keeps running and its mesh connection, on every connector and\non every space. The connector log records `AG-UI emitter stopped` with the reason and the delay\nbefore it re-establishes the plane: at once after a failure, then every second while the plane\nstays down, a fixed wait that neither grows nor is capped, so the plane is back within a second\nof the fault clearing. The re-establish\nis a fresh start: it reopens the write-ahead log, settles any frame it left pending, builds the\nmapper from the run the log has open, and reads on from the stored cursor, which is the same\nrecovery a connector restart runs. A failed publish or a failed log write is never retried on the\nemitter it failed on. A turn that ends while the plane is down keeps its terminal, and so does a\nturn whose terminal's own log write failed: once the re-establish has read on, the run the log had\nopen is closed with that terminal, a failure as a failure, rather than left open for the next\nprompt to close as a finish. That holds when the next prompt was already written before the plane\ncame back: the failure is published after the failed turn's own records and before the next run\nopens, in a frame of the failed run, since a run error names no run and the frame is its only\nattribution. Every turn that ends while the plane is down keeps its own terminal. A terminal belongs\nto the last turn the transcript had begun when its hook fired: the connector reports a turn's\nterminal once that turn's records are written and before the next turn's are, so the position the\ntranscript has reached at the hook names the turn, including a turn the connector opens no run for,\nsuch as a task notification, which takes its terminal and publishes nothing. A terminal repeated\nfor a turn that has one replaces nothing and is kept for no later turn. A line the connector cannot\nread, written past that position while the plane was down, does not hold the terminal: the\nre-establish reports the gap and places the terminal after the records before it. A record no read\nanswers, such as an OpenCode part still filling when its turn died, holds it no more: the terminal\nis placed after the records before it, and the turn end reads the session once rather than without end. The OpenCode source reads every record at or before the end it reported at a turn end as settled, so a part left filling by a failed turn does not stop the turns after it from publishing. A terminal reported while the first start is still waiting on the broker reaches the emitter before its first read, so a prompt written in that window does not close the failed turn as a finish. The same placement runs with\nthe plane up: a terminal reported while the flush before it is still reading, with the next prompt\nwritten before that read, closes its own turn in the place of the finish that prompt synthesizes. The seat used to exit on a space that requires events; for Claude Code the\nconnector is the MCP server, so that exit showed as the server disconnecting, and Claude Code\nrestarted it on the next tool call.\n\nA transcript line that is not a record does not stop the plane. Claude writes the transcript, and a\nwrite cut short by a full disk leaves part of one record glued to the next on a single line, which\nnever becomes readable. The connector steps over that line, keeps the records after it, and writes\n`skipped an unreadable transcript record at offset <offset> (<bytes> bytes)` with the parser's\nreason to its stderr.\n\nA new session includes its first run even when Claude writes a positional startup prompt before the\nconnector receives `SessionStart`. That from-zero read is keyed only to Claude's explicit\n`source: \"startup\"`; resumed, forked, cleared, and compacted sessions adopt at the transcript boundary\ncaptured at that adopt, before the mesh link connects, so nothing Claude appends while the connector\nis still starting up lands behind the cursor and is silently dropped. Crash recovery follows the\ncursor already stored in the event write-ahead log, regardless of the new process's startup label.\n\nClaude starts each hook in its own process, so a prompt or stop relay can reach Cotal before the\n`SessionStart` relay. The connector holds those event flushes and the terminal until `SessionStart`\nsupplies the source, then enqueues adopt, flush, and close in that order.\n\n`SessionStart` can also run before the connector process has bound its local control socket. The hook\nthe `SessionStart` relay retries only transient pre-connect listener errors, with capped backoff\ninside its existing two-second budget. Later hooks and permanent local faults still fail open\nimmediately. Once a socket has connected, a broken exchange is not retried: the connector may\nalready have handled the frame, so replaying it could apply one lifecycle event twice.\nThat retained `SessionStart` can itself arrive before Claude creates the transcript path. A genuinely\nnew startup waits up to five seconds for that file with capped backoff, and the same deadline bounds\none stalled file read; expiry fails loud instead of silently losing the first run. A forked session\ngets the same wait, because Claude copies the parent transcript into the fork's own file after the\nhook, and then adopts at the end of that copy. Resumed, cleared and compacted starts and recovered\ncursors still require their existing source at once.\n\nTool arguments (`TOOL_CALL_ARGS`) and tool results (`TOOL_CALL_RESULT`) are not republished\nonto this channel. The durable emitter drops those events before they are written to the\nwrite-ahead log, because this channel's read ACL is not the ACL the tool ran under. Content is\nmandatory on both kinds, so the event is suppressed rather than emptied or replaced with a\nplaceholder. Tool start and end still go out. A restart that finds a pending pre-fix frame\nstill carrying those kinds HALTS rather than republishing it.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non that channel alone. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel is that session's event stream. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, so it refuses a grant that\nleaves off any of the three ACL flags. Only `--full` turns an omitted flag into the wide default\n(`>` read, `>` post, `spawn,role:default` scope), which is the opposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal auth mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nOn an **open** mesh there is nothing to grant: the mesh has no credentials and no ACLs, so any peer\nthat lists the channel reads it, and the refusal says so instead of naming a command. The\nown-channel rule still applies there, because a spawn is not the place to hand out a read on\nanother agent's tool inputs and outputs.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`: an endpoint reads the channels it lists, so one constructed without\nthe event channel joins nothing and the frames never arrive. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal. On a user-auth mesh the actor half is the\nagent's own name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**Failed turns publish run errors.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the fixed message `run failed` and no code. Neither the detail Claude Code\nreported nor its error kind is published there: both are upstream values that can echo your prompt or\ntool output, and the events channel has a different read ACL. The error kind still reaches presence\nas the agent's condition (`rate_limit`, `auth`, `billing` and the rest). A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nA delayed initial connection or a broker reconnect pauses event publication without closing MCP.\nThe recorder waits for both the mesh binding and its transport; unpublished records stay in the\ntranscript until the connection recovers. Stopping the connector cancels that wait. A recording error,\nincluding a conflicting transcript or damaged event state, stops the publishing plane and not the\nsession: it is reported with the delay before the next re-establish, on every space, and the seat keeps\nits mesh connection. A second transcript path offered to a bound recorder is refused the same way, and\nthe first path stays bound.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, the default event plane needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected. Passing `events: false` is the explicit opt-out.\n\nArming the event plane through a typed spawn request (`manager.spawn` with `events`, including\nthe CLI's `cotal spawn --detach --events`) is allowed on a user mesh when the child runs under\nthe caller's owner. This also satisfies a registration policy that requires the plane. A spawn\nunder another owner still needs the caller's admin tier. Without it, an explicit request or a\nrequired plane is refused before provisioning. A silent non-owner caller in a space without that\npolicy has the plane disarmed with a notice in a successful reply. Later provisioning can still\nrefuse the spawn. The child's own-channel rule and the ledger's delegation envelope still apply.\n\n## Resume a session\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id> --on <instance>` carries a session held on *your* machine to that\n manager instance, which may run on another host. The CLI finds the transcript under your\n Claude config (`~/.claude`, or `$CLAUDE_CONFIG_DIR`), sends it through a JetStream Object\n Store bucket only that instance reads, under a writer credential pinned to that one transcript,\n and prints `carried session <id> to <instance>:\n sha256:<hex>, <sent> of <size> bytes sent in <chunks> chunks`. Without `--agent`, the CLI first asks that\n instance which harness the persona resolves to, so a persona with `agent: claude` is carried even\n when your `COTAL_DEFAULT_AGENT` names another connector. A re-run of the same bytes\n sends nothing, and an interrupted carry continues where it stopped. The seat forks it in a\n private Claude home under the manager's `.cotal/seat-homes/`, which no other seat's Claude\n lists or finds, and which is removed when the seat stops. When Claude starts the fork, the seat\n records the SHA-256 of the transcript it read from its own project; the manager stops a seat\n whose record names other bytes than the carried ones, or that records none within the join\n timeout after it joins, an uncertain launch included, and otherwise shows that record as the\n seat's provenance. `cotal attach` to such a seat names its source after the seat\n name, as `(resumed from <host>:<id>)`. A remote manager receives a carry when its host issues it a\n transfer reader. On a user-auth mesh the CLI exchanges the operator's login for a one-object\n `transfer-writer` view, which needs scope `admin`.\n- A session name in place of an id is refused, listing each session on this host that carries\n that name with its id, SHA-256 and modification time. An id this host does not hold resolves\n against the **manager host's** `~/.claude`, as before.\n- A seat-private home holds no login. The manager host needs `CLAUDE_CODE_OAUTH_TOKEN` (from\n `claude setup-token`), `ANTHROPIC_AUTH_TOKEN`, or a cloud provider selection in its\n environment; `ANTHROPIC_API_KEY` alone is refused. The private home trusts the authorized\n launch directory without changing the manager's home. Claude must be 2.1.234 or later.\n- The manager waits for a real outcome: `\u2713 started` means the agent *joined the mesh*,\n `\u2717 exited on launch` carries Claude's last output and the seat's exit diagnostic, such as a\n startup confirmation that never appeared, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Only the Claude connector supports it today; OpenCode and Hermes fail loud.\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nA spawned session loads the MCP servers your normal Claude Code configuration enables on the\nlaunch host: user, local, project, plugin and connected-account sources. Claude handles discovery,\nprecedence, disabled servers, project approval and managed policy. Cotal adds its mesh server\nand any overrides declared in `connectors.claude.mcpServers` ([format](config.md)). It does not\ncopy native server definitions or credentials into the launch file or onto another machine.\nThe selected `CLAUDE_CONFIG_DIR` remains in effect for an ordinary local spawn.\n\nFirst-run setup still seeds eligible user-scope declarations into the Cotal config and preserves\nan existing list. That list is not the source of truth for native discovery. A same-named declared\noverride takes precedence; changing the native definition does not rewrite an override you kept.\nAn empty Cotal sharing map leaves native discovery enabled.\n\nUse `--share-tools tavily,figma` to load only those declared servers plus Cotal, or\n`--share-tools none` for Cotal alone. These explicit selections use `--strict-mcp-config`.\nCarried sessions also stay strict and keep their seat-private home; operator profiles and\nconnected-account credentials are not copied into them. Native managed policy still applies.\n\nThe child environment stays allowlisted. References in declared shared servers and the selected\nuser-owned MCP configuration forward their named variables. Repository and plugin files cannot\nadd ambient secret names merely by referencing them; supply such names through an explicit\nshared declaration or the existing `spawn.env` policy. Native files retain their own configured\nvalues. Documented native MCP switches, including the connected-account opt-out and tool-search\nchoice, keep their explicit values. Cotal does not forward the entire parent environment or force\ntool definitions to load upfront.\n\nSharing a credential grants it to the session, so enable only servers the session may use.\nEach enabled server may start a process per seat; an explicit sharing selection can reduce that\ncost. Native discovery does not change Cotal's mesh identity, hooks or channel wake path.\n\n### One Cotal path per session\n\nA spawned session is on its mesh through the local Cotal server. A connected-account source can\nalso give it a hosted Cotal connector, which would join the same session a second time under\nanother name. The plugin's `PreToolUse` hook refuses every `cotal_*` tool of any server other than\nthe local one in a session Cotal launched, with the reason in the refusal. It decides in the hook\nprocess, so the refusal holds when the control socket is down. A Claude Code session that Cotal did\nnot launch keeps the hosted connector. Where the plugin is installed, its local server's\ninstructions tell the model to use the local path unless you ask for the hosted one.\n\nIn such a session the plugin also keeps each hosted connection with the session that opened it. A\n`PostToolUse` hook on the hosted `cotal_connect` records the returned `connection_id` under the\nplugin's data directory (`CLAUDE_PLUGIN_DATA`), and `cotal_disconnect` releases it. A fork made with\n`--fork-session` inherits that id in its transcript but has a new session id, so its calls with the\nid are refused and it is told to call `cotal_connect`. A resumed session and its subagents keep the\nsession id and pass. An id the plugin never saw opened goes to the hosted connector, which checks\nthe account.\nThe guard runs only where the `cotal` plugin is installed. A Claude Code session with only the\nhosted connector has no hook, so nothing records its connections and its forks are not refused.\n\nA hosted connector cannot start a turn, so a hosted conversation hears a message only when it calls a\ntool. `cotal_wait` holds until a message that would wake the agent arrives and returns it, or returns\nwith none after the service's bound. The conversation calls it again to keep listening. Every other\nhosted tool adds a line to its result when messages are waiting. A conversation with no turn running\nis not woken.\n\n## Host sessions in one process\n\nA host serving many managed seats can run their MCP servers in one process with the subpath\n`@cotal-ai/connector-claude-code/mcp`, which loads with both `import` and `require`:\n\n```ts\nimport { serveClaudeSession } from \"@cotal-ai/connector-claude-code/mcp\";\n\nconst session = await serveClaudeSession({ env, input, output });\nawait session.close();\n```\n\nLoading the module only exports it; it serves its own stdio only when run, as `node dist/mcp.cjs`.\nThe subpath ships CommonJS declarations so TypeScript hosts can use either module format,\nincluding CommonJS consumers with Node16 or Node18 module settings.\nClaude Code pushes `claude/channel` only to a stdio server it launched, so each `claude` still\nlaunches a stdio command, such as a small relay that pipes its stdio to the host.\n\n- **Managed only.** `env` is a complete launch environment, copied when the call starts. It must\n carry a mesh identity and the full control pair, or the call rejects before the session joins the\n mesh. The presence environment label, bearer commands, carried-fork records and feedback contact\n lookup use that copy. Feedback's Git fallback reads the session's global config, including its\n includes, without reading the host repository config. Set `COTAL_FEEDBACK_EMAIL` or pass the\n feedback tool's `email` argument to choose another contact. Include the `PATH`, `HOME` and other OS\n variables subprocesses need; the host's environment is not merged into them. Subprocesses still\n use the host process's working directory. A session's card does not: its `meta.cwd` is the `cwd`\n its own `SessionStart` hook reports on startup, resume or fork, absent until then, and unchanged by\n clear or compact. It is advisory display data that every reader of the space's roster can see. A plain `claude` with no identity is served by `mcp.cjs` run directly. Sharing a process\n does not isolate untrusted tenants or their files.\n- **Streams.** `input` carries JSON-RPC from the client and `output` carries this session's replies\n and `claude/channel` pushes. The caller owns both streams. Input that has emitted `end` or is\n destroyed rejects startup. Buffered unread input remains eligible. When an active session's\n `input` emits `end` or `close`, the session closes, including after a relay connection resets.\n Teardown may pause `input` and removes the transport's error handler. A host that reuses the\n stream must resume it as needed and keep its own stream error handlers.\n- **`fatalBind`.** Off by default: a control socket that cannot be bound rejects the call and the\n host keeps its other sessions. Only a process that is the session turns it on, as `mcp.cjs` does.\n- **Teardown.** `close()` closes the control socket, the wake policy, the AG-UI emitter, the mesh\n endpoint and the MCP server, and is idempotent. It removes its input listeners without closing\n the caller's streams. The returned promise rejects if teardown fails. A startup failure closes\n what was started before the call rejects. A manager shutdown frame or input EOF or closure closes the\n session and then calls `onShutdown(code)` once. A teardown failure is logged and reported with\n code 1, without becoming an unhandled rejection in the host. Close waits for a pending broker\n dial to settle. A dial that fails after shutdown starts is not a teardown failure. The session\n releases local inbox custody only after its connections are proved closed, so the same lifecycle\n can reopen after a successful close. An active duplicate or an unproved close retains custody.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
27105
|
+
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Its lifecycle hook imports the relay from the\n`@cotal-ai/connector-core/relay` subpath, so each hook process loads the relay and its environment\nreaders and none of the NATS client, zod or yaml. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Hermes](connect-hermes.md) (alpha), [pi](connect-pi.md) (alpha); the\n[Connectors](connectors.md) matrix compares them feature-by-feature.\n\n## Set up\n\n> **Team and Enterprise accounts:** [Enable Claude Code channels](#enable-channels-for-your-organization)\n> before running unattended agents. Channels are blocked by default, so incoming messages cannot\n> wake an idle session until an organization Owner enables them.\n\n```bash\ncotal setup --configure-only # install the plugin and seed one agent without startup\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin for your user account, so every Claude Code session on\nthis machine gets the `cotal_*` tools, in any project. That covers the terminal, the IDE extensions\nand the Desktop app's local Code sessions. Setup can use Desktop's bundled Code executable when no\nterminal `claude` command is installed. Desktop-only users should open a local Code session once so\nDesktop downloads that executable. Setup checks it through the connector's own readiness probe;\nmanaged `spawn` still requires the terminal `claude` command. With a local mesh already running, run setup once, reopen Claude\nand ask it to join your local mesh. No MCP configuration edits or special launch flags are\nneeded for this native plugin path. It also shares your own MCP servers with spawned sessions on its first run (see\n[Sharing your MCP servers](#sharing-your-mcp-servers)), and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nA repeat `cotal setup` and `cotal update` refresh the copy under `~/.cotal/claude-plugin` and update\nexisting Claude plugin registrations, including older cached installations. After an npm upgrade,\nthe first regular Cotal command also refreshes these copies before accepting the new generation. Setup,\nskills installation and refresh share a machine-local lock through the final plugin check, so simultaneous\ncommands cannot overwrite a generation while another command verifies it. Disabled installations\nstay disabled during an update. `cotal status` checks the resolved payload's manifest version and\nfile contents. A folder-backed plugin can retain older version metadata in Claude's installation\nrecords; that metadata does not override the files Claude loads. Running sessions keep the\ncode they loaded; reload plugins or reopen Claude to load the update.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide). The Claude connector declares and implements this\n setup provider, including the marketplace assets and native plugin commands; the base CLI only passes\n the vendor-neutral Agent Skills directory. The plugin carries no code and no core dependency,\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release. `cotal update` refreshes an existing skills plugin without\n enabling it or installing a removed one; `cotal setup --skills` also installs missing skills. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind. The same provider reports the plugin and skills plugin rows\n in `cotal status`, which point a stale or missing skills plugin at `cotal setup --skills`.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it\n (and `cotal setup --skills` does only that):\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile, and names `cotal setup --skills` as the remedy. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Enable channels for your organization\n\nFor claude.ai Team and Enterprise organizations, channels are blocked by default. An organization\nOwner must enable them in [Organization settings > Claude Code > Channels](https://claude.ai/admin-settings/claude-code),\nor set this organization-managed setting:\n\n```json\n{\n \"channelsEnabled\": true\n}\n```\n\nUsers cannot override this setting through local or project settings. Cotal's\n`--dangerously-load-development-channels` launch flag cannot override it either. Enable channels\nbefore launching the sessions that need them.\n\nWhen channels are blocked, Cotal's MCP tools and hook-based inbox delivery still work when the agent\ntakes a turn. An incoming message cannot wake an idle Claude Code session. A successful spawn or a\nconnected MCP server does not prove idle wake is available; check Claude's startup notice before\nleaving agents unattended.\n\nPersonal Pro and Max accounts without an organization do not need this admin opt-in. Sessions still\nneed to opt into their channel, which Cotal's managed launch does for its own server. See\n[Anthropic's channel controls](https://code.claude.com/docs/en/channels#enterprise-controls) for\norganization policy and other authentication modes.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona` / `cotal_personas`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches its declared capabilities. `cotal_run` is gated separately by\n`run`; use `capabilities: [spawn, run]` for both. Fresh setup defaults include both.\nSee [workflow tool setup](workflows.md#from-an-agent-session) for a first run and missing-tool checks.\nClearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | `http` lifecycle hooks \u2192 POST to the connector (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal \\\n --permission-mode bypassPermissions\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Tool permissions.** A managed seat has no human at its terminal, so it launches with\n `--permission-mode bypassPermissions`: a prompt under the default mode would stop its first mesh\n tool call until someone answered it, and nobody can. To get prompts back, pass\n `--opt permission-mode=default` (or another mode); the connector adds the default only when the\n launch options carry neither `permission-mode` nor `dangerously-skip-permissions`. Claude refuses\n the bypass mode as root and under a policy that disables it; such a host must pass a mode.\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set:\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **Persona privacy.** The persona body is written to a private file and Claude receives only\n `--append-system-prompt-file <path>`. The body never appears in the spawned process argv. The\n carrier is a 0600 file inside a 0700 directory on POSIX, with equivalent owner-only ACL hardening\n on Windows. That is OS-user isolation: any process running as your user can read it while it\n exists. The manager or the foreground `cotal spawn` removes it, and the shared-server MCP config\n file, once it has proved the `claude` process gone. If the launcher is killed first, a watcher\n started beside `claude` removes them when `claude` exits.\n- **MCP servers.** A spawned agent loads Claude's native MCP sources alongside Cotal and declared\n sharing overrides. Explicit `--share-tools` selections and carried private homes keep strict\n isolation. Claude still applies its own server approvals, disabled settings and managed policy\n (see below).\n- **Lean MCP server.** Every session runs its own cotal MCP server, so Node starts it with\n `--optimize-for-size --max-semi-space-size=1` to shrink its memory footprint. Managed launches and\n the plugin's `.mcp.json` (personal plugin sessions) both pass these flags. `--lite-mode` and\n `--jitless` would save more, but they remove `WebAssembly`, which Node's built-in `fetch` needs,\n so WebAssembly stays enabled.\n- **Light hook.** Claude Code starts the plugin's hook as a new process for every tool call, prompt\n and stop. The hook imports `@cotal-ai/core` only through small subpaths such as\n `@cotal-ai/core/peer-label`, never the package root, which would bundle YAML and NATS into it.\n `pnpm smoke:claude-hook-weight` holds that.\n- **Installed plugin.** The plugin is installed once, at user scope (`claude plugin install\n cotal@cotal-mesh --scope user`), because its hooks bind only to an *installed* plugin. Earlier\n releases installed it per project, at local scope; the next `cotal setup` removes those installs and\n keeps the user-scope one. If Claude Code cannot change a project's settings, setup still removes\n the others, then fails naming that project and the `claude plugin uninstall` command to run there. A session Cotal launches loads the package's own copy with `--plugin-dir`,\n which replaces the installed one for that session, so it still runs one cotal server.\n The repo's `.claude-plugin/marketplace.json` lists the committed plugin tree under\n `claude-plugin/`, which each release regenerates with the built bundles, the skills and the\n release version, so an install from the repo or from a pinned commit runs without a build\n ([Release](release.md)). `cotal setup` (npx, no clone) materializes the same marketplace under\n `~/.cotal/claude-plugin/` from the installed CLI (each plugin dir is rebuilt from scratch and\n atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its manifest and install behavior ship inside the Claude connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** A launched session joins under the identity in `COTAL_NAME`, `COTAL_LINK`\n or `COTAL_AGENT_FILE`. A plain `claude` with none of them joins only when its conversation calls\n `cotal_connect`, so your own sessions do not appear as stray peers. Until then its MCP\n server builds no mesh agent and opens no broker connection. See\n [Sessions Cotal did not launch](#sessions-cotal-did-not-launch).\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt. The runtime waits for the\n dialog title in normalized terminal text and presses Enter once when it appears, so startup speed\n does not affect a supervised launch. Claude skips that prompt when channels are off for the\n session, for example under a Team or Enterprise organization whose managed settings do not set\n `channelsEnabled: true`, and opens its input screen instead. The runtime then ends its watch\n without a key once the input screen's footer shows `(shift+tab to cycle)` or `? for shortcuts` on\n two screen reads in a row. That seat gets mesh messages through its hooks only, so a message does\n not wake it while it is idle. The PTY runtimes read the child's screen, and the tmux, cmux, Orca\n and Herdr runtimes read the pane's screen. If neither the prompt nor the input screen appears\n within 15 seconds, the seat ends with a bounded error naming the unmatched prompt instead of\n hanging silently or answering another dialog. The PTY runtime writes that error to the seat's output and\n stops the child as a graceful stop does, so a child that ignores SIGTERM is killed 3 seconds\n later. The other runtimes write the error to the manager's log.\n- **Trusted directory.** An authorized supervised spawn accepts the selected workspace through\n Claude's native trust dialog, which Claude draws in one of two layouts. When a numbered list\n shows Yes first and selected, the runtime confirms it with Enter. When No comes first and is\n selected, the runtime moves to Yes and confirms only after seeing Yes selected. It never confirms\n while No is selected, sends each response once, and stops watching after the\n development-channel confirmation or on the input screen. A changed or unsupported dialog receives no guessed input\n and still fails within the startup bound. Claude saves trust with its own repository and linked\n worktree rules; Cotal does not rewrite the manager's configuration. Accepting trust enables the\n workspace's hooks and settings, which can select a tool permission mode when no launch flag\n overrides it. Cotal adds no tool-permission bypass flag. Review the checkout before authorizing\n a spawn there. A foreground `cotal spawn` keeps the trust dialog in your terminal.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\nThe consuming connector holds a crash-safe claim for its broker target, space, principal and\nlifecycle on this OS user account. A second patched session on the same host refuses before\npulling that inbox, even when its launch files and control socket differ. Reconnect keeps the\nclaim. Hooks and non-consuming helper connections do not claim it. The control listener binds\nbefore the broker connection and never removes an existing socket path.\n\nThis is cooperative same-host containment. Other hosts, broker URL aliases and older clients\nare not fenced. Preserve the existing session before choosing a distinct actor for a new\nlaunch. An uncertain shutdown keeps custody until the consuming process exits.\n\n## Sessions Cotal did not launch\n\nA Claude Code session you start yourself, where the `cotal` plugin is installed, gets the tools the\nhosted connector offers: `cotal_connect`, `cotal_wait`, `cotal_disconnect` and the routed mesh tools.\nNothing joins until the conversation calls `cotal_connect` with a name. The plugin's server then runs\n`cotal app-identity provision` with the CLI that `cotal setup` recorded beside the plugin\n(`dist/cli.json`), so it does not need `cotal` on `PATH`. Each conversation gets an identity of its\nown on the mesh a bare `cotal spawn` from the session's directory would pick. A name that a live peer\nholds moves to `<name>_2`, `<name>_3` and so on. The identity's channels and role come from the mesh's\ndefault persona. `cotal_disconnect`, the end of the session or a signal retires it, and the next\nprovision retires one whose server was killed. A plugin that `cotal setup` did not install has no\nrecord, and `cotal_connect` names that remedy.\n\nThe gateway's MCP annotations distinguish read-only queries from updates. Orientation, connection\nstatus, the roster and channel information are read-only. Sending messages, connecting and joining\nchannels are additive. Inbox reads and waits consume messages; status, channel mode, leaving and\ndisconnecting can replace or remove state. These hints do not grant access or override Claude's\npermission settings or an organization's approval policy.\n\nA message for a connected conversation starts a turn through the plugin's\n[native hooks module](https://code.claude.com/docs/en/plugins/mods/overview). This works in supported\nClaude Code hosts, including local sessions in the Desktop app's Code tab. The module runs a local\nwake reader as a child of the same host that runs the MCP server. Each server writes connection ids\nto its own socket in a private directory. The module submits the fixed notification\n`Cotal: a new message for connection_id <id>. Read it with cotal_inbox.` with Claude's plugin-sender\nattribution. It never submits it as the person's own words. While Claude is busy, the module appends\nthe notification at the next main-conversation tool or model-request boundary instead of queuing a\nsecond turn after the work ends. Peer message contents stay in the inbox. Between turns, the\ntranscript shows a compact **Cotal \xB7 new message** indicator; click it to inspect the full\nnotification. This changes only the display, not the message Claude receives.\n\nA wake follows the rule `cotal_wait` follows. It comes once until the conversation reads its messages,\nand not while a `cotal_wait` holds the connection. A replacement reader reoffers unread work.\nClaude's Stop and organization controls still apply. A refused wake stays pending without retrying\non every new message. `cotal_connect` reports whether the incoming reader is attached; attachment\nalone does not prove that the host admitted a turn. When native modules are unavailable or blocked,\nthe conversation can listen with `cotal_wait` instead.\n\nClaude Desktop chat does not run a plugin's local server. On macOS and Windows, where Claude Desktop\nis installed, `cotal setup` adds a `cotal` server to Desktop's own config,\n`claude_desktop_config.json` (Settings > Developer > Edit Config), and keeps every other entry there.\nIt runs the same server with `--desktop`, by the absolute paths of the node that runs the CLI and of\nthe plugin copy under `~/.cotal/claude-plugin`. Quit and reopen Claude Desktop to load it. A repeat\n`cotal setup` and `cotal update` keep the entry current, `cotal status` reports it, and a config\nfile that is not valid JSON is refused and left as it was. Setup can use Desktop's bundled Code\nexecutable when there is no standalone `claude` CLI. Its installed MCP and command hooks use the\nrecorded Node executable, so the app need not find Node on `PATH`. On Linux, setup writes nothing\nfor Desktop Chat.\n\nDesktop's working directory is not your project, so its mesh is the `cotal use` mesh, else the only\none running. Desktop runs no monitor, so a Desktop conversation hears messages when it calls a tool,\nand keeps calling `cotal_wait` when you ask it to keep listening. A chat with no turn running cannot\nbe woken by any server: Claude's chat surfaces start a turn only from the person.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n A hook whose handler throws still returns an empty reply so the session is never blocked, and\n that reply carries nothing, so it commits nothing: the batch it had started to surface stays\n un-acked and goes out on a later frame. The seat also drops any `turn-pending` row that breaks\n the manager contract, such as one with no integer deadline, and says so once in its log. A reply\n with no `turns` array changes nothing: the seat keeps the turns it already holds.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. When the channel becomes active, the connector\n first re-fires a focus mention remembered during startup, otherwise one buffered wake. A rejected\n push keeps its bounded retry, and JetStream redelivery remains the durable backstop for unacked\n inbox items. Neither a redelivery nor that retry repeats a nudge already pushed for that message,\n whether the message had its own nudge or was counted in a batch one, so a session held in a long\n tool call gets one nudge per message. Once a hook frame carries the message, or the push that\n announced it fails, its next redelivery nudges again, so a reply that never reached Claude Code\n still recovers. If the channel cannot run at all, delivery still waits for the next hook. Live-only\n traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is delivered in parts: once no smaller mail is waiting,\neach call carries its next part, and it is cleared only after its last part goes out, because clearing\nwhat was not handed over is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items first, then other\nchannel traffic, and a direct message or role request only when the whole buffer is directed mail.\nIt never evicts a message the connector is handing to the session or a turn is still carrying.\nAn evicted channel item is acknowledged. An evicted direct message or role request never is, and\nthe broker redelivers it after the ack wait until a redelivery finds room. A direct message stays\npending on the session's DM durable, where `cotal deliver pending <name>` counts it. A role request\nstays on its role's shared queue, which that command does not read. A full inbox therefore delays\ndirected mail until the session drains it.\nIf the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content. Recall cannot tell one message with an\nempty id from an identical one with another disposition, so in focus such a message is held in the\nlocal inbox as pull-only instead of being dropped, and a mention of it still wakes the agent. When\nthe session settles an id-less copy, it reads the chat stream's last sequence. Identical copies\narrive in stream order, and those reads can answer out of order, so the first read that can see the\ncopies binds them in arrival order, latest first, each to the latest unbound stream copy at or below\nthe lowest sequence read for it or any later identical copy.\nWhile the connection stays up, every stream copy at or below that sequence reached the session\nfirst, so a later identical copy sent during a reconnect gap is above it and stays unbound. A copy\nthat arrives while a read runs may not be in that read, so it binds nothing there and recall reads\nthe channel again, up to three times; if it still could be a stream copy in the last read, recall\nleaves that stream copy in the stream and reports the channel as incomplete. A settled copy a\ncomplete read cannot bind is behind the focus start or out of retention, and is forgotten. Recall\nhands back into the inbox only the stream copies nothing is bound to, such as one sent during a\nreconnect gap or one the inbox evicted on overflow, and `cotal_inbox` hands each over once. Overflow\nfrees only the evicted copy, held or quiet, and a copy no read has bound yet keeps its place in\narrival order, so an identical muted copy stays out of recall. When\nthe inbox is full, recall leaves them in the stream for a later call and reports the channel as\nincomplete. A history read that fails, or a channel with replay off, settles nothing and is reported\nas incomplete, and recall calls run one at a time. If the sequence read for a settled copy fails,\nor answers only after the connection dropped, recall skips that channel for the rest of the focus\nperiod and reports it as incomplete. Recall cannot tell a late copy of a message it handed back from\na new identical message, so every copy takes its own disposition: a new identical quiet mention is\nstill delivered automatically, and a late copy can surface a second time.\nA recalled message that already went out in part is read to its last part, even if an exclusion\nlands after its first part. One session reads its inbox one call at a time: a `cotal_inbox` call\nthat overlaps another waits for it to finish, so neither decides from a view the other has already\nmoved past.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\nWhatever does reach a turn is framed so a peer cannot write the frame. A line that begins at column\nzero is written by the connector; one message is one line plus indented continuations, with the\nsender inside a single bracket pair. A message body, a sender name and role, and a service or\nchannel label are all peer-controlled, so each passes through the same neutralization the\n`cotal_inbox` reply uses: no line break a splitter may honour and no bracket survives into a\nrendered attribution. This matters more for an injected block than for a reply, because the agent\ndid not ask for it and so never had the chance to distrust it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\nA `SessionStart` during an open turn, including compaction, preserves the current `working` or\n`waiting` status until `Stop`, `StopFailure`, or `SessionEnd` closes the turn.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` only when no turn is open (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `PostToolBatch` | `working` when the main agent's batch resolves after a prompt left the seat `waiting`, which clears its condition so the turn's `Stop` yields; a subagent's batch (it carries `agent_id`) or one relayed after the turn's terminal changes nothing |\n| `Notification` (`permission_prompt` / `agent_needs_input`) | `waiting` with condition `approval` / `input` (activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). `StopFailure` also relays Claude Code's native error value as `condition.source` and maps it to the closed condition vocabulary. On the [event plane](#event-plane) it closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nThe connector also leaves gracefully when its stdin closes. An MCP client closes it to end the\nsession, and a killed `claude` closes it with no `SessionEnd`, so a dead session drops off the\nroster instead of staying on it as a live peer.\n\n`StopFailure` maps `rate_limit` and `overloaded` directly; auth and credential failures to\n`auth`; account and billing failures to `billing`; `invalid_request` to `request`;\n`model_not_found` to `model`; `server_error` to `server`; `max_output_tokens` to `context`; and\n`unknown` to `failed`. The native value remains in `condition.source`.\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. Claude Code's own\n[hooks reference](https://code.claude.com/docs/en/hooks) lists every hook event. The hooks the\nplugin registers, with their matchers, are listed in the\n[adapter README](../extensions/connector-claude-code/README.md#claude-code-hooks).\n\n## Event plane\n\nA spawned session publishes a **structured** account of what it\ndid: run boundaries per turn, assistant text, reasoning, and each tool call with its start\nand its end. Not prose about the work, the work itself, in a vocabulary a program can\nread. The launcher sets `COTAL_EVENTS` by default; pass `--no-events` to opt out on an unrestricted\nspace. A user-auth registration with `policy: { events: \"required\" }` carries `eventsRequired` in the\nprivate launch material, so the connector arms even without `COTAL_EVENTS`; `--no-events` is refused.\nA hand-driven user-mode session may carry the same decision as `COTAL_EVENTS_REQUIRED=1`. Its own\npublish grant must cover `events.<owner>.<actor>` or the connector refuses before joining. An unmanaged\nsession with no launch material and no required-policy fallback keeps the generic default behavior.\n\nIf the event plane stops, the seat keeps running and its mesh connection, on every connector and\non every space. The connector log records `AG-UI emitter stopped` with the reason and the delay\nbefore it re-establishes the plane: at once after a failure, then every second while the plane\nstays down, a fixed wait that neither grows nor is capped, so the plane is back within a second\nof the fault clearing. The re-establish\nis a fresh start: it reopens the write-ahead log, settles any frame it left pending, builds the\nmapper from the run the log has open, and reads on from the stored cursor, which is the same\nrecovery a connector restart runs. A failed publish or a failed log write is never retried on the\nemitter it failed on. A turn that ends while the plane is down keeps its terminal, and so does a\nturn whose terminal's own log write failed: once the re-establish has read on, the run the log had\nopen is closed with that terminal, a failure as a failure, rather than left open for the next\nprompt to close as a finish. That holds when the next prompt was already written before the plane\ncame back: the failure is published after the failed turn's own records and before the next run\nopens, in a frame of the failed run, since a run error names no run and the frame is its only\nattribution. Every turn that ends while the plane is down keeps its own terminal. A terminal belongs\nto the last turn the transcript had begun when its hook fired: the connector reports a turn's\nterminal once that turn's records are written and before the next turn's are, so the position the\ntranscript has reached at the hook names the turn, including a turn the connector opens no run for,\nsuch as a task notification, which takes its terminal and publishes nothing. A terminal repeated\nfor a turn that has one replaces nothing and is kept for no later turn. A line the connector cannot\nread, written past that position while the plane was down, does not hold the terminal: the\nre-establish reports the gap and places the terminal after the records before it. A record no read\nanswers, such as an OpenCode part still filling when its turn died, holds it no more: the terminal\nis placed after the records before it, and the turn end reads the session once rather than without end. The OpenCode source reads every record at or before the end it reported at a turn end as settled, so a part left filling by a failed turn does not stop the turns after it from publishing. A terminal reported while the first start is still waiting on the broker reaches the emitter before its first read, so a prompt written in that window does not close the failed turn as a finish. The same placement runs with\nthe plane up: a terminal reported while the flush before it is still reading, with the next prompt\nwritten before that read, closes its own turn in the place of the finish that prompt synthesizes. The seat used to exit on a space that requires events; for Claude Code the\nconnector is the MCP server, so that exit showed as the server disconnecting, and Claude Code\nrestarted it on the next tool call.\n\nA transcript line that is not a record does not stop the plane. Claude writes the transcript, and a\nwrite cut short by a full disk leaves part of one record glued to the next on a single line, which\nnever becomes readable. The connector steps over that line, keeps the records after it, and writes\n`skipped an unreadable transcript record at offset <offset> (<bytes> bytes)` with the parser's\nreason to its stderr.\n\nA new session includes its first run even when Claude writes a positional startup prompt before the\nconnector receives `SessionStart`. That from-zero read is keyed only to Claude's explicit\n`source: \"startup\"`; resumed, forked, cleared, and compacted sessions adopt at the transcript boundary\ncaptured at that adopt, before the mesh link connects, so nothing Claude appends while the connector\nis still starting up lands behind the cursor and is silently dropped. Crash recovery follows the\ncursor already stored in the event write-ahead log, regardless of the new process's startup label.\n\nClaude starts each hook in its own process, so a prompt or stop relay can reach Cotal before the\n`SessionStart` relay. The connector holds those event flushes and the terminal until `SessionStart`\nsupplies the source, then enqueues adopt, flush, and close in that order.\n\n`SessionStart` can also run before the connector process has bound its local control socket. The hook\nthe `SessionStart` relay retries only transient pre-connect listener errors, with capped backoff\ninside its existing two-second budget. Later hooks and permanent local faults still fail open\nimmediately. Once a socket has connected, a broken exchange is not retried: the connector may\nalready have handled the frame, so replaying it could apply one lifecycle event twice.\nThat retained `SessionStart` can itself arrive before Claude creates the transcript path. A genuinely\nnew startup waits up to five seconds for that file with capped backoff, and the same deadline bounds\none stalled file read; expiry fails loud instead of silently losing the first run. A forked session\ngets the same wait, because Claude copies the parent transcript into the fork's own file after the\nhook, and then adopts at the end of that copy. Resumed, cleared and compacted starts and recovered\ncursors still require their existing source at once.\n\nTool arguments (`TOOL_CALL_ARGS`) and tool results (`TOOL_CALL_RESULT`) are not republished\nonto this channel. The durable emitter drops those events before they are written to the\nwrite-ahead log, because this channel's read ACL is not the ACL the tool ran under. Content is\nmandatory on both kinds, so the event is suppressed rather than emptied or replaced with a\nplaceholder. Tool start and end still go out. A restart that finds a pending pre-fix frame\nstill carrying those kinds HALTS rather than republishing it.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non that channel alone. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel is that session's event stream. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, so it refuses a grant that\nleaves off any of the three ACL flags. Only `--full` turns an omitted flag into the wide default\n(`>` read, `>` post, `spawn,role:default` scope), which is the opposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal auth mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nOn an **open** mesh there is nothing to grant: the mesh has no credentials and no ACLs, so any peer\nthat lists the channel reads it, and the refusal says so instead of naming a command. The\nown-channel rule still applies there, because a spawn is not the place to hand out a read on\nanother agent's tool inputs and outputs.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`: an endpoint reads the channels it lists, so one constructed without\nthe event channel joins nothing and the frames never arrive. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal. On a user-auth mesh the actor half is the\nagent's own name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**Failed turns publish run errors.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the fixed message `run failed` and no code. Neither the detail Claude Code\nreported nor its error kind is published there: both are upstream values that can echo your prompt or\ntool output, and the events channel has a different read ACL. The error kind still reaches presence\nas the agent's condition (`rate_limit`, `auth`, `billing` and the rest). A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nA delayed initial connection or a broker reconnect pauses event publication without closing MCP.\nThe recorder waits for both the mesh binding and its transport; unpublished records stay in the\ntranscript until the connection recovers. Stopping the connector cancels that wait. A recording error,\nincluding a conflicting transcript or damaged event state, stops the publishing plane and not the\nsession: it is reported with the delay before the next re-establish, on every space, and the seat keeps\nits mesh connection. A second transcript path offered to a bound recorder is refused the same way, and\nthe first path stays bound.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, the default event plane needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected. Passing `events: false` is the explicit opt-out.\n\nArming the event plane through a typed spawn request (`manager.spawn` with `events`, including\nthe CLI's `cotal spawn --detach --events`) is allowed on a user mesh when the child runs under\nthe caller's owner. This also satisfies a registration policy that requires the plane. A spawn\nunder another owner still needs the caller's admin tier. Without it, an explicit request or a\nrequired plane is refused before provisioning. A silent non-owner caller in a space without that\npolicy has the plane disarmed with a notice in a successful reply. Later provisioning can still\nrefuse the spawn. The child's own-channel rule and the ledger's delegation envelope still apply.\n\n## Resume a session\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id> --on <instance>` carries a session held on *your* machine to that\n manager instance, which may run on another host. The CLI finds the transcript under your\n Claude config (`~/.claude`, or `$CLAUDE_CONFIG_DIR`), sends it through a JetStream Object\n Store bucket only that instance reads, under a writer credential pinned to that one transcript,\n and prints `carried session <id> to <instance>:\n sha256:<hex>, <sent> of <size> bytes sent in <chunks> chunks`. Without `--agent`, the CLI first asks that\n instance which harness the persona resolves to, so a persona with `agent: claude` is carried even\n when your `COTAL_DEFAULT_AGENT` names another connector. A re-run of the same bytes\n sends nothing, and an interrupted carry continues where it stopped. The seat forks it in a\n private Claude home under the manager's `.cotal/seat-homes/`, which no other seat's Claude\n lists or finds, and which is removed when the seat stops. When Claude starts the fork, the seat\n records the SHA-256 of the transcript it read from its own project; the manager stops a seat\n whose record names other bytes than the carried ones, or that records none within the join\n timeout after it joins, an uncertain launch included, and otherwise shows that record as the\n seat's provenance. `cotal attach` to such a seat names its source after the seat\n name, as `(resumed from <host>:<id>)`. A remote manager receives a carry when its host issues it a\n transfer reader. On a user-auth mesh the CLI exchanges the operator's login for a one-object\n `transfer-writer` view, which needs scope `admin`.\n- A session name in place of an id is refused, listing each session on this host that carries\n that name with its id, SHA-256 and modification time. An id this host does not hold resolves\n against the **manager host's** `~/.claude`, as before.\n- A seat-private home holds no login. The manager host needs `CLAUDE_CODE_OAUTH_TOKEN` (from\n `claude setup-token`), `ANTHROPIC_AUTH_TOKEN`, or a cloud provider selection in its\n environment; `ANTHROPIC_API_KEY` alone is refused. The private home trusts the authorized\n launch directory without changing the manager's home. Claude must be 2.1.234 or later.\n- The manager waits for a real outcome: `\u2713 started` means the agent *joined the mesh*,\n `\u2717 exited on launch` carries Claude's last output and the seat's exit diagnostic, such as a\n startup confirmation that never appeared, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Only the Claude connector supports it today; OpenCode and Hermes fail loud.\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nA spawned session loads the MCP servers your normal Claude Code configuration enables on the\nlaunch host: user, local, project, plugin and connected-account sources. Claude handles discovery,\nprecedence, disabled servers, project approval and managed policy. Cotal adds its mesh server\nand any overrides declared in `connectors.claude.mcpServers` ([format](config.md)). It does not\ncopy native server definitions or credentials into the launch file or onto another machine.\nThe selected `CLAUDE_CONFIG_DIR` remains in effect for an ordinary local spawn.\n\nFirst-run setup still seeds eligible user-scope declarations into the Cotal config and preserves\nan existing list. That list is not the source of truth for native discovery. A same-named declared\noverride takes precedence; changing the native definition does not rewrite an override you kept.\nAn empty Cotal sharing map leaves native discovery enabled.\n\nUse `--share-tools tavily,figma` to load only those declared servers plus Cotal, or\n`--share-tools none` for Cotal alone. These explicit selections use `--strict-mcp-config`.\nCarried sessions also stay strict and keep their seat-private home; operator profiles and\nconnected-account credentials are not copied into them. Native managed policy still applies.\n\nThe child environment stays allowlisted. References in declared shared servers and the selected\nuser-owned MCP configuration forward their named variables. Repository and plugin files cannot\nadd ambient secret names merely by referencing them; supply such names through an explicit\nshared declaration or the existing `spawn.env` policy. Native files retain their own configured\nvalues. Documented native MCP switches, including the connected-account opt-out and tool-search\nchoice, keep their explicit values. Cotal does not forward the entire parent environment or force\ntool definitions to load upfront.\n\nSharing a credential grants it to the session, so enable only servers the session may use.\nEach enabled server may start a process per seat; an explicit sharing selection can reduce that\ncost. Native discovery does not change Cotal's mesh identity, hooks or channel wake path.\n\n### One Cotal path per session\n\nA spawned session is on its mesh through the local Cotal server. A connected-account source can\nalso give it a hosted Cotal connector, which would join the same session a second time under\nanother name. The plugin's `PreToolUse` hook refuses every `cotal_*` tool of any server other than\nthe local one in a session Cotal launched, with the reason in the refusal. It decides in the hook\nprocess, so the refusal holds when the control socket is down. A Claude Code session that Cotal did\nnot launch keeps the hosted connector. Where the plugin is installed, its local server's\ninstructions tell the model to use the local path unless you ask for the hosted one.\n\nIn such a session the plugin also keeps each hosted connection with the session that opened it. A\n`PostToolUse` hook on the hosted `cotal_connect` records the returned `connection_id` under the\nplugin's data directory (`CLAUDE_PLUGIN_DATA`), and `cotal_disconnect` releases it. A fork made with\n`--fork-session` inherits that id in its transcript but has a new session id, so its calls with the\nid are refused and it is told to call `cotal_connect`. A resumed session and its subagents keep the\nsession id and pass. An id the plugin never saw opened goes to the hosted connector, which checks\nthe account.\nThe guard runs only where the `cotal` plugin is installed. A Claude Code session with only the\nhosted connector has no hook, so nothing records its connections and its forks are not refused.\n\nA hosted connector cannot start a turn, so a hosted conversation hears a message only when it calls a\ntool. `cotal_wait` holds until a message that would wake the agent arrives and returns it, or returns\nwith none after the service's bound. The conversation calls it again to keep listening. Every other\nhosted tool adds a line to its result when messages are waiting. A conversation with no turn running\nis not woken.\n\n## Host sessions in one process\n\nA host serving many managed seats can run their MCP servers in one process with the subpath\n`@cotal-ai/connector-claude-code/mcp`, which loads with both `import` and `require`:\n\n```ts\nimport { serveClaudeSession } from \"@cotal-ai/connector-claude-code/mcp\";\n\nconst session = await serveClaudeSession({ env, input, output });\nawait session.close();\n```\n\nLoading the module only exports it; it serves its own stdio only when run, as `node dist/mcp.cjs`.\nThe subpath ships CommonJS declarations so TypeScript hosts can use either module format,\nincluding CommonJS consumers with Node16 or Node18 module settings.\nClaude Code pushes `claude/channel` only to a stdio server it launched, so each `claude` still\nlaunches a stdio command, such as a small relay that pipes its stdio to the host.\n\n- **Managed only.** `env` is a complete launch environment, copied when the call starts. It must\n carry a mesh identity and the full control pair, or the call rejects before the session joins the\n mesh. The presence environment label, bearer commands, carried-fork records and feedback contact\n lookup use that copy. Feedback's Git fallback reads the session's global config, including its\n includes, without reading the host repository config. Set `COTAL_FEEDBACK_EMAIL` or pass the\n feedback tool's `email` argument to choose another contact. Include the `PATH`, `HOME` and other OS\n variables subprocesses need; the host's environment is not merged into them. Subprocesses still\n use the host process's working directory. A session's card does not: its `meta.cwd` is the `cwd`\n its own `SessionStart` hook reports on startup, resume or fork, absent until then, and unchanged by\n clear or compact. It is advisory display data that every reader of the space's roster can see. A plain `claude` with no identity is served by `mcp.cjs` run directly. Sharing a process\n does not isolate untrusted tenants or their files.\n- **Streams.** `input` carries JSON-RPC from the client and `output` carries this session's replies\n and `claude/channel` pushes. The caller owns both streams. Input that has emitted `end` or is\n destroyed rejects startup. Buffered unread input remains eligible. When an active session's\n `input` emits `end` or `close`, the session closes, including after a relay connection resets.\n Teardown may pause `input` and removes the transport's error handler. A host that reuses the\n stream must resume it as needed and keep its own stream error handlers.\n- **`fatalBind`.** Off by default: a control socket that cannot be bound rejects the call and the\n host keeps its other sessions. Only a process that is the session turns it on, as `mcp.cjs` does.\n- **Teardown.** `close()` closes the control socket, the wake policy, the AG-UI emitter, the mesh\n endpoint and the MCP server, and is idempotent. It removes its input listeners without closing\n the caller's streams. The returned promise rejects if teardown fails. A startup failure closes\n what was started before the call rejects. A manager shutdown frame or input EOF or closure closes the\n session and then calls `onShutdown(code)` once. A teardown failure is logged and reported with\n code 1, without becoming an unhandled rejection in the host. Close waits for a pending broker\n dial to settle. A dial that fails after shutdown starts is not a teardown failure. The session\n releases local inbox custody only after its connections are proved closed, so the same lifecycle\n can reopen after a successful close. An active duplicate or an unproved close retains custody.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
27106
27106
|
},
|
|
27107
27107
|
{
|
|
27108
27108
|
"slug": "connect-codex",
|
|
@@ -27227,8 +27227,8 @@ function loadDocsBundle() {
|
|
|
27227
27227
|
"slug": "release",
|
|
27228
27228
|
"title": "Publishing a release",
|
|
27229
27229
|
"kind": "Project (non-normative maintainer notes)",
|
|
27230
|
-
"summary": "Cotal uses Changesets
|
|
27231
|
-
"body": "# Publishing a release\n\n> **Project** (non-normative maintainer notes) \xB7 **For:** maintainers shipping Cotal\n\nCotal uses [Changesets](https://github.com/changesets/changesets) to version its public workspace\npackages. The fixed group in [`.changeset/config.json`](../.changeset/config.json) must match every\npublic workspace package. Examples are not published.\n\n## Publication deadline\n\nThe standing publication deadline is ten minutes from the human publish command to verified\nregistry availability. Record that command's UTC timestamp immediately. Merging, entering a runner,\nuploading packages and retrying must not reset it. One owner runs publication.\n\nFinish source review, versioning, native qualification and any release-readiness holds before the\ncommand. Do not add serial reviews, unrelated changes, another native matrix or advisory-CI waits\nafter it. Keep security, credential and repository protections. Report a platform refusal or a\nmissed deadline rather than calling an accepted upload a published release.\n\nGitHub runner admission and npm propagation are external dependencies. The workflow records the\noriginal deadline and any breach; it cannot guarantee those services' response times. A delayed\nrelease can finish under the original authorization, but is never reported as timely. Each execution\nattempt has one ten-minute timeout recorded before checkout. Later steps cannot restart that timeout.\nA local smoke or dry run is not production evidence for the ten-minute target.\n\n## CI policy\n\nAutomatic smoke shards exclude mutation infrastructure and print each exclusion. Build, typecheck,\nprotocol smokes and native cold setups still run. The Mutation reproof and Windows creation identity\nworkflows are disabled because they execute mutation proofs. Do not re-enable them without an\nexplicit operator request.\n\n## Trusted publishing\n\nThe main-only `version` job publishes normal and snapshot releases through GitHub OIDC. It uses the\n`npm-publish` Environment. Create that Environment with `main` as its allowed deployment branch\nbefore the first publication. A configured approval hold is a readiness blocker, not permission to\ndisable protection to meet the deadline.\n\nEvery public package needs a trusted publisher configured on npmjs.com:\n\n- Organization or user: the repository owner.\n- Repository: `Cotal`.\n- Workflow filename: `changesets.yml`.\n- Environment name: `npm-publish`.\n\nUse `https://www.npmjs.com/package/<name>/access` and **Trusted publishing > Add a trusted publisher**.\nRepeat for every public package. A former publisher with a blank Environment must be replaced with\none naming `npm-publish`. Snapshots use the same main-only publisher, not a less-protected branch.\n\nInspect the derived census without writing packages:\n\n```bash\nnode scripts/preflight-npm-publish.mjs\n```\n\nWithout the GitHub OIDC requester, an absent-version census refuses before publication. An\nall-present census returns a no-op before OIDC. The preflight rejects npm access-token environment\nvariables before invoking pnpm, so an OIDC refusal cannot fall back to a classic token.\n\n### Adding a publishable package\n\nA new public package needs its npm record and trusted publisher before release preparation finishes.\n\n1. Its owner publishes the first version with `npm publish --access=public` and an npm token. OIDC\n cannot create a package. This is separate from `ci:publish`, which refuses npm tokens.\n2. Configure the package's trusted publisher with `changesets.yml` and `npm-publish`.\n3. Check that the package appears in the complete preflight census.\n\n## Preparing a release\n\n1. Add a changeset with each public package change. Packages are 0.x, so a breaking change is minor.\n List every affected first-party package and describe the change. Check whether an older branch's\n change has already shipped before keeping its changeset.\n2. Merge reviewed source to `main`. The required `attribution` check needs a current\n `Approved-at: <sha>` paragraph in the PR body. Add it after reviewing that head, outside code\n blocks and HTML comments. A new push requires a new review and paragraph. Advisory checks report\n results but do not require waiting for terminal-green CI.\n3. The push-only `Changesets` job opens or updates the version PR. `pnpm ci:version` bumps package\n versions, updates changelogs, rebuilds the Claude connector and materializes the committed plugin\n bundles and manifests. The action rewrites the PR body, so add its current approval after its last\n update. Keep generated operator-literal counts current when their examples change.\n4. Finish the version PR's native `Cold setup` run and independent source reviews. Use a successful\n first attempt, not artifacts mixed across partial reruns. A fresh run is the remedy for an\n ineligible rerun. Merge the eligible version PR before publish-go.\n5. Save the v2 qualification receipt's version and run ID, candidate manifest SHA-256, public\n signature bundle, original source and source tree, immutable artifact IDs, all native results and\n the approved release-source commit on main. The qualified PR merge snapshot can differ from that\n commit only when their complete Git trees match. Keep original native build records; do not\n rewrite their source to the merge SHA.\n\nA frozen main source can also be prepared with `gh workflow run cold-setup.yml --ref main` before\npublish-go. Use this when the version PR's qualified merge snapshot no longer matches its main\nmerge. Main movement after the frozen source does not require another qualification.\n\nMerging the version PR no longer publishes automatically. A push prepares versions; an explicit\npublication dispatch promotes qualified bytes. Preparation and publication use separate queues, so\na build cannot hold the publish command behind it. Publication dispatches share one non-cancelling\nqueue and cannot overlap or evict one another.\n\nPrepare a snapshot separately, before its publish command:\n\n```bash\ngh workflow run cold-setup.yml --ref main -f snapshot=next\n```\n\nThe snapshot is versioned once before qualification. Publication replays those versions, including\ntheir timestamp, rather than generating another version. Its qualification must name the same main\nrelease-source commit. A changed candidate source or tree invalidates readiness. Later main merges\ndo not: the publisher uses the frozen source rather than silently widening the requested scope.\n\n## Publishing the prepared candidate\n\nOn the human command, retain its original timestamp and dispatch the prepared main candidate:\n\n```bash\ngh workflow run changesets.yml --ref main \\\n -f \"version=<qualified-version>\" \\\n -f \"qualification_run=<run-id>\" \\\n -f \"candidate_sha256=<manifest-sha256>\" \\\n -f \"source=<approved-main-sha>\" \\\n -f \"requested_at=<original-UTC-timestamp>\"\n```\n\nFor a prepared snapshot, also pass `-f snapshot=next`, matching its qualification. A timestamp such\nas `2026-10-22T09:15:00Z` is UTC. Do not replace the original command time with the dispatch time.\n\nGitHub records the version, candidate digest, source, qualification run, tag and original UTC in the\ndispatch name. Publication enumerates complete main-dispatch history, partitioning capped searches,\nand binds later attempts to the earliest workflow run number for that version. A fresh dispatch\ncannot replace its clock or requested scope. Keep those records; deleting history is not recovery.\nThe first human timestamp is still the maintainer's assertion. CI does not independently observe\nthe human command or make that history resistant to repository administrators.\n\nThe trusted publisher controls come from the dispatch's main commit. The requested release source\nmust be that commit or an older first-parent main commit. An owned temporary detached worktree\nprovides its manifests without a second clone or build. A merged side-branch head is not accepted\nas a main version carrier. Qualification must match the frozen source's full tree, not the newer\npublisher controls' tree. All three source identities are retained in the outcome.\n\nThe publisher checks the live GitHub run and immutable artifact selection. It rejects forks,\nforeign repositories and workflows, failed or unfinished runs, partial reruns, expired or ambiguous\nartifacts, invalid completion timestamps, a changed candidate digest and a different source tree.\nQualification should finish before the command. If preparation was late, a successful qualification\nof the frozen source is still required before any write; the original command time stays unchanged. A PR run's API head is checked against the qualified merge snapshot's head parent;\nit is not relabelled as the checkout. All 18 native receipts retain their original source and\ncandidate hash.\n\nThe candidate build attests its manifest through a pinned first-party GitHub action. Its public\nSigstore bundle binds the manifest to certificate claims about the source commit, signer workflow\ncommit and path, hosted runner, run ID and attempt. Promotion verifies those claims through `gh`\nand reads invocation identity from the verified certificate. Predicate fields and a claimed Git\nparent relationship cannot establish the executed source. Native qualification still requires all\n18 receipts and a successful complete run. Forks retain native checks but cannot publish.\n\nPreparation exercises the live certificate verifier with positive and wrong-source/run/attempt\ncontrols before native qualification. The runner needs a `gh` version supporting artifact-attestation\nverification and its source, signer and hosted-runner policy flags. Old unsigned qualifications are\nineligible; prepare a signed v2 candidate before the command.\n\nThe privileged workflow downloads by artifact ID and qualifying run ID with `actions: read`.\nIt does not rebuild packages, assemble different native binaries or run package hooks after go.\nnpm's provenance identifies the publication job. The qualification receipt separately identifies\nthe original build; same-tree promotion does not claim a new build at the merge SHA.\n\n## Publication controls\n\nThe candidate packs every public package. Before packing it assembles native binaries, builds,\nruns the package-shape probe and runs every public `prepublishOnly` guard in dependency order.\nPacking runs pack hooks and hashes archive entries without extracting them.\n\nNative setups install that archive set into empty homes, config and caches on Linux, macOS and\nWindows, x64 and arm64, with Node 22, 24 and 26. They check installed bytes, connector seeding and two\nbroker, delivery and Manager lifecycles. Required evidence includes native process pins, control,\none-shot shutdown, cleanup and fresh process generations after restart. Missing evidence blocks\nqualification. The final `cold-qualified` artifact is written only after all prerequisites succeed.\n\n`ci:publish` runs `scripts/publish-cold-candidate.mjs`. Before the first write it checks:\n\n- The complete public workspace census matches the Changesets fixed group.\n- Every exact version is absent and every npm package record exists. Mixed or unknown results refuse.\n- The requested dist-tag does not already point to a newer version of any package. Stale prepared\n candidates and unreadable tag metadata refuse before authorization, rather than moving a tag back.\n- Every package has a successful package-specific GitHub OIDC exchange.\n- A readable trusted-publisher document grants direct publication to this repository's\n `changesets.yml` publisher in `npm-publish`. A stage-only or foreign publisher refuses. npm's trust\n endpoint can return 401 because it requires an npm access token; that is recorded as unverifiable,\n not proof of a direct-publish permission. Other non-200 answers refuse.\n- Every source manifest, archive path and archive hash matches qualification. Hashes are checked\n again at each write boundary.\n\nRegistry and authorization reads use bounded parallel work. At most four archive uploads run at\nonce. `cotal-ai` starts only after all sibling uploads succeed. Each upload uses:\n\n```bash\npnpm publish <archive> --provenance --access=public --no-git-checks --ignore-scripts --tag <tag>\n```\n\nThe tag is `latest` or the qualified snapshot tag. Package scripts already ran during preparation.\nThe publisher runs on the POSIX GitHub runner. A live supervisor keeps ownership of each client's\nprocess group until client completion. The owner then terminates and observes that group, including\nsame-group descendants whose wrapper exited with ignored stdio. The execution attempt's deadline\nalso stops the group. It admits no new work after an error and waits for admitted groups to settle. Unobserved\nsettlement prevents further admission and leaves installation scratch in place for investigation. Receipts distinguish not-started,\nrunning, accepted, refused and unknown outcomes. Accepted means the publisher client completed,\nnot that consumers can install the version. An interrupted running outcome requires investigation.\n\npnpm batch publishing is not used. It requires a registry-specific endpoint and shared credential,\nand rejects the provenance path used here.\n\n## Verifying availability\n\nBefore announcing a release, the workflow downloads every registry archive, compares its SHA-256\nwith the qualified bytes, checks its registry SHA-512 integrity and verifies every requested\ndist-tag. It then installs `cotal-ai@<version>` into a fresh private home, config and cache and runs\n`cotal --version`. Its environment excludes operator credentials and personal state. Upload and\ninstall commands own their child groups and wait for closure before scratch cleanup. Normal\ncancellation stops those groups and prevents new admissions. All stages share the execution\nattempt's ten-minute timeout, including the install check. Availability is measured separately\nagainst the original command time.\n\nThe normal release retains the full-closure metadata gate. Its `UNSETTLED` and `NONE` outcomes do not\ncreate a GitHub Release. Publication fails unless actual archive and install verification complete.\nLate verification records a failed timing target and emits a warning, but does not prevent the\nverified release from becoming available or receiving its announcement. Accepted uploads alone\ncannot pass verification.\nPublication outcomes and immutable artifact identities are uploaded even on failure when the runner\ncan finish cleanup.\n\nAfter successful verification, the normal release creates its GitHub tag and Release. It targets\nthe oldest first-parent main commit whose `bin/package.json` carries that version, including a\nversion reverted and later carried again. Unreadable history or a missing carrier refuses the tag.\nSnapshot publication does not create that normal-release announcement.\n\n## Recovering late registry visibility\n\nDo not blindly repeat an upload, reset the command timestamp or start another native matrix.\nThe publication workflow refuses write attempts on a rerun; a fresh dispatch is not permission to\nignore unknown outcomes either. Retain the qualified candidate and publication outcomes. Recheck\nregistry metadata without writing:\n\n```bash\nnode scripts/verify-publish-closure.mjs <version> --recheck\nnode scripts/verify-release-installable.mjs <version>\n```\n\nRecheck the retained archive set and dist-tag against the original request:\n\n```bash\nnode scripts/verify-promoted-archives.mjs \"<candidate>/manifest.json\" latest \"<output>.json\" \\\n --recheck \"--requested-at=<original-UTC-timestamp>\" \"--candidate-sha256=<manifest-sha256>\"\n```\n\nUse the qualified tag instead of `latest` for a snapshot. This read-only mode has a bounded recovery\nobservation window while preserving the original command time and candidate digest. Its receipt\nreports late availability as outside the publication budget. Recovery never turns a missed deadline\ninto a timely release. If packages remain missing, inspect the write outcomes and registry evidence\nbefore deciding on another write. Once availability is verified, the publication owner can finish a\nmissing announcement under the original release authorization.\n\n## Correcting a released changelog entry\n\nAdd a `**Correction:**` paragraph under the existing bullet in every affected `CHANGELOG.md`, indented\ntwo spaces. Preserve the original published text. Use identical correction wording so\n`scripts/release-notes-detail.mjs` can deduplicate it.\n\n## 0.11 runtime migration\n\nThe binary no longer bundles optional tmux and cmux runtimes. Existing operators run\n`cotal ext add <runtime-package>` once before using that runtime. Missing runtimes fail with their\ninstallation command instead of falling back to pty.\n"
|
|
27230
|
+
"summary": "Cotal uses pnpm, Changesets and one GitHub Actions workflow.",
|
|
27231
|
+
"body": "# Publishing a release\n\n> **Project** (non-normative maintainer notes) \xB7 **For:** maintainers shipping Cotal\n\nCotal uses pnpm, Changesets and one GitHub Actions workflow. The fixed group in\n[`.changeset/config.json`](../.changeset/config.json) versions the public packages together.\nExamples are private.\n\n## Prepare the version\n\nAdd a changeset for each package change. Breaking changes in these 0.x packages use a minor bump.\nThe Changesets workflow opens or updates the version PR after a change reaches `main`.\nMerge the intended version update before publishing.\n\nKeep builds and focused upgrade checks ready before the publish command. A successful fresh install\ndoes not prove an upgrade from an existing installation. CI, review rounds and the broad native test\nmatrix are not publication prerequisites unless the operator requests them.\n\n## Publish\n\nRun the workflow on the versioned `main` commit:\n\n```bash\ngh workflow run changesets.yml --ref main\n```\n\nThe workflow uses the maintained Changesets actions to select the release, pack its public packages\nand publish the packed artifacts. GitHub Actions pins the run to one commit. One publication runs at\na time. There is no separate qualification or promotion workflow.\n\nLinux and Windows build jobs produce Cotal's native binaries. The pack job assembles those binaries\nand builds the packages without npm publishing authority. Only the publish job has the npm trusted\npublishing permission. It consumes the packed output rather than rebuilding it.\n\nThe root `ci:publish` command is the standard `pnpm changeset publish`. Maintainers normally use the\nworkflow so publication has the configured identity and the native binaries are included.\n\n## Verify the result\n\nThe publish action reports the packages published and creates their GitHub releases. The workflow\nthen installs the published CLI into an isolated prefix and runs it. Check that result and the\nrelevant existing-installation upgrade before reporting the fix delivered.\n\nAn accepted upload and a successful install are separate observations. npm may temporarily return\ncached missing-package responses after an upload. If an observation fails, inspect the registry and\nthe published-package output before retrying. Never blindly repeat an upload whose outcome is unknown.\n\nKeep the original publish-command time. The target is verified availability within ten minutes;\nreport a missed target rather than resetting the clock or calling a queued job published.\n\n## Configure npm\n\nEvery public npm package needs this trusted publisher:\n\n- Repository owner and repository: this repository.\n- Workflow filename: `changesets.yml`.\n- Environment: `npm-publish`, allowing `main`.\n\nThe workflow uses GitHub OIDC and does not fall back to a long-lived npm token. A missing package or\npublisher authorization is an access problem, not permission to borrow credentials.\n\n## Correct release notes\n\nAdd a `**Correction:**` paragraph under the affected changelog bullet. Preserve the original text\nand use the same correction wording in each affected package so release notes can deduplicate it.\n"
|
|
27232
27232
|
},
|
|
27233
27233
|
{
|
|
27234
27234
|
"slug": "roadmap",
|
|
@@ -27284,7 +27284,7 @@ function loadDocsBundle() {
|
|
|
27284
27284
|
"title": "Upgrading a running deployment",
|
|
27285
27285
|
"kind": "Guide (informative)",
|
|
27286
27286
|
"summary": "Substrate stability tells you what the version numbers promise.",
|
|
27287
|
-
"body": "# Upgrading a running deployment\n\n> **Guide** (informative) \xB7 **For:** operators upgrading a mesh that already exists \xB7 **See also:** [Substrate stability](stability.md), [Run a mesh](run-a-mesh.md), [Identity and auth](identity-and-auth.md)\n\n[Substrate stability](stability.md) tells you what the version numbers promise. This page is the\nother half: what to actually do when the deployment already exists, has credentials in it, and\ncannot simply be recreated. Every release that breaks a running deployment gets a section here,\nnaming what migrates on its own, what does not, and the order to move the pieces in.\n\n## The pre-1.0 upgrade contract\n\nThe packages are pre-1.0, so a minor bump may break an API or an on-disk expectation. Four\ncommitments make that survivable for someone with a fleet:\n\n- **Pin an exact version.** `0.N.P`, never `^0.N.P`. A range can pull a breaking minor in during an\n unrelated reinstall.\n- **Every break that touches a running deployment gets a section on this page**, written in terms of\n what an operator does, not in terms of which module changed.\n- **Read the section before you start, not halfway through.** A section names the work up front\n precisely so the operation does not change shape once it is underway.\n- **A break that cannot be made automatic says so.** Where credentials or state must be recreated by\n hand, the section says which ones and when, rather than leaving you to discover it at the moment\n the first one stops working.\n- **A change to the shape of a credential, or to who may renew one, is breaking whatever the commit\n marker says.** This rule is stated because the marker is a judgement made while writing the code\n and the consequence is felt by someone running it a day later. A fleet that keeps authenticating\n looks compatible and is not, if nothing in it can renew. Any automated check of this rule would\n read commit markers, so a break recorded as a feature is the one case it could not see, which is\n why the rule is written for people first. **The marker held for this release: the 0.49.0 change\n that caused all of this, `36d177951 feat(core)!`, did carry its `!`.** The rule exists for the\n next one that does not.\n\nWhat this page does not promise is a rolling upgrade. Nothing in the current line dual-serves two\nauthority versions, so where broker and manager run separately there is a window in which the mesh\nis down. The sections below give that window's shape so it can be scheduled rather than endured.\n\n## Cooperative workflow cancellation in 0.92.0\n\nThe manager adds the issued, instance-bound `run-cancel` command. Its cluster revision advances to\n37, retaining the vitals and keyed-submission commands. Cancellation is available through the\ngeneric endpoint client; this change adds no CLI or MCP cancellation shortcut.\n\n### What keeps working\n\nExisting run records, admissions, revocation markers and journals keep their formats. Upgrade does\nnot rewrite them, cancel a run or remove a marker. A released run without a marker can still resume\nunder its admission. A revoked run remains revoked. Manager shutdown still requests boundary\nrelease; it does not write a cancellation marker or prove an agent process stopped.\n\n### What needs an update\n\n- Custom `RunHost` implementations must return a drive with `cancel(reason)`, in addition to\n `done` and `release(reason)`. It must propagate cooperative root cancellation to in-flight\n effects. Do not substitute boundary release or process termination. Rebuild and qualify the\n custom host against the new core contract before enabling it on an upgraded manager.\n- The built-in runtime and language packages carry the root signal through both engines and the\n compiled worker. Keep core, auth, manager, runtime and language on the same release; copying one\n source file or replacing only the manager does not supply the new protocol and worker code.\n- Existing caller credentials do not gain a new publish permission on installation. Obtain fresh\n issued credentials through the supported issuer before using `run-cancel`. Static credentials\n and caller generations must still meet the original lifecycle and grant checks. Never edit a\n credential to add the permission.\n- The new command requires a fresh Describe-derived instance and epoch bind. An old issuer lacks\n its independent request observer and inspect/commit path, so a new signerless manager cannot use\n that issuer for cancellation. Do not treat missing support as a reason to bypass the issuer.\n\n### Order for a split topology\n\nSchedule a maintenance window for the version change; this is not a rolling-compatibility promise.\nTake the deployment's usual consistent backup of broker state, including run records, admissions\nand journals, and the issuer's private configuration. Upgrade the issuer, then the manager and its\nruntime dependencies to the same release, using their normal stop/start procedure. Reinstalling a\npackage does not replace modules already loaded by a running process. Reconnect callers through\nthe upgraded issuer and resolve the manager's current contract before invoking cancellation.\nThere is no broker format migration for this command and no automatic cancellation during upgrade.\n\n### What the result establishes\n\nA durable marker denies future admission but is not proof of cessation. Only `stopped` with the\ncaptured drive outcome establishes that local drive's result. If the holder recheck fails or the\nrequest budget expires, a parked effect can remain running beside a committed marker. Preserve an\nunknown result; do not blindly resend it or declare cleanup complete. To continue cancellation,\na caller can make a fresh explicit request, which observes the marker and current holder before\nreporting its own result. Pending cancellation intent can also block a local launch after a definite\nno-dispatch refusal if marker absence was not confirmed within the budget.\n\n`run show` now displays revocation independently of recorded state. An unreadable marker yields\na partial result and exit 4 in human and JSON output, rather than an apparently complete view.\nAutomation must handle that existing partial-result convention. None of these observations proves\nmodel or tool preemption, physical process cleanup or cessation on another host.\n\n## Service authority status selector in 0.92.0\n\nA service endpoint authority status query that reads one operation now names that operation:\n`target: { kind: \"operation\", operation, requestId }`, where `operation` is `register`,\n`activate`, `renew`, `ready` or `revoke`. `@cotal-ai/core` exports the operation names as\n`ServiceStatusOperation` and the whole selector as `ServiceStatusTarget`. The 0.91.0 selector,\n`{ kind: \"operation\", requestId }`, no longer parses. The change is in `parseServiceAuthorityRequest`,\n`parseServiceAuthorityEnvelope` and the `ServiceAuthorityRequest` type, so it reaches every client\nand server built on them.\n\nA `revoke` operation status parses, but the 0.92.0 auth service does not answer it yet: it is\n`denied` with reason `unsupported` (HTTP 422), as SPEC states. To follow a revocation, read\n`target: { kind: \"instance\" }`, which reports `revoking` with the revoke's operation id.\n\n### What keeps working\n\n- Every running mesh. 0.91.0 shipped the request types and the client (the auth provider's\n `serviceEndpointAuthority` hook and its HTTP client), but no 0.91.0 auth service serves\n `/service-endpoint-authority`, so no deployment has status traffic on this route to lose. In\n 0.92.0 only a context opened with `serviceEndpoints` serves the route. That option is off by\n default and has no CLI flag; without it the route answers 404.\n- `target: { kind: \"instance\" }` is unchanged, and so are the register, activate, renew, ready and\n revoke requests themselves.\n- No credential, broker or connection is involved. Nothing needs reminting and no broker restarts.\n\n### What does not migrate\n\n- **Code that builds an operation status query without `operation`.** The 0.92.0 provider hook and\n client parse each request before sending it, so the query fails locally with a `bad-request`\n error, `service-endpoint authority: <detail>`, whose detail is\n `object has missing or unsupported fields`, and nothing is sent. An operation name outside the\n five gets the detail `unsupported status operation`.\n- **A 0.91.0 client still sends the old selector.** A 0.92.0 auth service opened with\n `serviceEndpoints` answers it with HTTP 400, whose body carries the same parser message. The\n service reads and writes nothing for it and logs nothing. The 0.91.0 provider hook and client\n never show that message: they cannot parse the 400 body as a protocol result, so they throw a\n `ServiceAuthorityTransportError` whose message is\n `service authority returned an unconfirmed response; retain the existing request identity`, with\n `httpStatus` 400 and outcome `not-executed`. A status query is a read, so nothing ran and\n nothing needs replaying. An auth service without `serviceEndpoints` answers 404 before it reads\n the body.\n- **No legacy operation is inferred.** Neither the client nor the service maps an old selector onto\n `register` or any other operation. One requestId can key a different request under each\n operation, so a guessed operation could answer with another request's status. Send the operation\n the request was made as; the requestId you stored with it stays the same.\n\n### Durable data\n\nNothing stored changes on upgrade, and nothing is rewritten. The selector is a request shape and is\nnever stored. 0.91.0 wrote no service authority request intents or operation records, so there is\nnothing to convert. A 0.92.0 context opened with `serviceEndpoints` writes its own records from the\nmoment it is enabled: an admission record when it first opens, then request intents, the\nper-instance intent index, operation, proposal and assignment records, and endpoint gates. Each\nintent already records its operation. Without `serviceEndpoints` it writes none of them.\n\n### Order for a split topology\n\nUpgrade every client that builds status queries together with, or before, the first auth service\nyou open with `serviceEndpoints`. That means code using `ServiceAuthorityRequest` or\n`parseServiceAuthorityRequest`, and every process that calls the auth provider's\n`serviceEndpointAuthority`. This change adds no ordering for brokers, managers or connectors;\norder those as their own sections say.\n\nThere is no compatibility window to schedule. A 0.92.0 auth service never accepts the old selector,\nand a 0.91.0 auth service has no handler for the route at all, so a mixed pool is not partly\nworking: every 0.92.0 auth service opened with `serviceEndpoints` refuses an old client's\noperation status queries with 400, which that client reports as the unconfirmed-response error\nabove, and one without `serviceEndpoints` answers 404. Nothing else is interrupted, so the change\nhas no outage window of its own.\n\n### Before the upgrade\n\n- Nothing needs a snapshot for this change, because it writes nothing. If the same upgrade first\n enables `serviceEndpoints`, take the deployment's usual consistent backup of broker JetStream\n state and the auth host's private configuration before that step, because the service endpoint\n host then starts writing to the auth bucket.\n- Find the old selectors in your code, then name the operation in each:\n\n```sh\ngrep -rn 'kind: \"operation\"' --include='*.ts' --include='*.js' src/\n```\n\n```ts\n// 0.91.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", requestId } }\n// 0.92.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", operation: \"register\", requestId } }\n```\n\nWhat this section measured, and what it did not: the local refusal and the accepted and refused\nselectors are covered by the core parser's own test. The 400, 404 and `unsupported` answers, and\nthe absence of a log line, are read from the auth service's source. The 0.91.0 client's error is\nread from the 0.91.0 client and core parser source; the client file is unchanged since 0.91.0. No\nmixed-version deployment was run.\n\n## History reader deletes after 0.86.0\n\nObserver and admin credentials now carry a `CONSUMER.DELETE` row for each of the connection's eight\nhistory-reader names, `hist_<readerId>_<n>`, on CHAT (and on DM for admin). `channelHistory`,\n`multiChannelHistory` and `dmHistory` read under those names and delete each reader when its step\nends. Before this, every read left its consumers for five minutes, and a dashboard polling history\nfilled the stream's consumer limit until reads were refused with \"maximum consumers limit reached\".\n\n### What stops working\n\nA static observer or admin credential minted by 0.86.0 or earlier has no reader-delete rows. On it,\na current client's history read fails with \"it predates the observer and admin reader-delete grant,\nso remint it\", and leaves one reader per stream until the broker's five-minute inactive threshold.\nA credential the CLI mints at start (`cotal console`, `cotal web` on a static auth mesh) is current\nafter the upgrade.\n\n### Before the upgrade\n\n- Remint every observer or admin credential a process reads from a file: `cotal auth mint <name> --profile\n observer` or `--profile admin`, and any `--creds` file given to a dashboard.\n- On a user-auth mesh, the auth service mints these views per connection. Upgrade it before, or with,\n the hosts that read history, so no current client connects through an older auth service.\n\n## Native key issuance recording after 0.85.0\n\nBefore upgrading, take a consistent backup of broker JetStream state and the auth host's private\nconfiguration and signing material through the deployment's supported backup procedure. The auth\nbucket must use file storage. Allow space for one issuance record per new callout exchange until\nits minted expiry plus clock tolerance; static credentials have no such record.\n\nEmbedders that call `startAuthCallout` must supply the required `recordIssuance` callback. It must\nfinish a durable create-only write for the broker-generated user key and recheck the validated\nlifecycle and credential authority before returning. A missing callback refuses startup; a failed\nwrite or recheck denies the exchange. Do not use the isolated tests' no-op callbacks in a deployment.\nThe stock `startAuthService` authority plane wires the recorder and its pruning automatically.\nExisting auth records need no manual rewrite, and no tenant or participant grant is added.\n\nFor split deployments, pause new managed launches and stop every old auth-callout responder before\nstarting the matched new auth service. A mixed responder pool can still issue keys without records.\nNew connections and reconnects cannot authenticate while the callout service is down. Existing\nbroker connections keep their current JWT expiry; this change needs no broker restart. Upgrade the\nCLI, Manager and connector packages together before admitting new managed launches, using the\nruntime's supported Manager custody or recovery path. This source change does not promise that\nrunning seats survive a Manager restart.\n\nThe new service prunes expired issuance rows before opening and on a timer. A corrupt row refuses\nopening or fences the running authority plane; investigate the stored evidence through the normal\noperator recovery procedure. Do not erase the auth bucket or recreate signing keys to clear it.\nRows written before a later authority-check failure can remain until pruning; they do not prove\nthat a client received a JWT or established a connection.\n\nConnections admitted before the last old responder stopped, static JWTs and old responders have no\nissuance record. They are not backfilled or identified from names. Bearers without current lifecycle\nand credential claims cannot be recorded and are refused; use the supported enrollment or renewal\npath for the selected release. These records alone do not provide persistent native-key cutoff or\ncomplete revocation. Reintroducing an old responder also reopens the recording gap.\n\n## Native Claude MCP discovery after 0.82.2\n\nBefore upgrading, snapshot Cotal configuration and the Claude profile on each launch host.\nOrdinary Claude spawns now load the host's native MCP sources alongside declared Cotal overrides.\nAn empty sharing map no longer suppresses native servers. Audit enabled servers and their\ncredentials before admitting new seats. Use an explicit sharing selection, including\n`--share-tools none`, to retain isolation. Existing sessions keep their loaded configuration.\n\nNo broker restart, credential-format migration or cross-host profile copy is required. Upgrade\nthe CLI, core, manager and connector together on each launch host. Pause new managed spawns\nwhile adopting the matched packages; use the runtime's supported manager custody or recovery\npath. Any interruption is that manager transition, not a change to the broker wire contract.\nCarried private homes stay isolated and do not import the operator's native profile.\n\n## Connector-store protection after 0.82.2\n\nSnapshot the private Cotal state, connector store and mesh configuration before upgrading.\nInstall the new CLI on each CLI and manager host, then verify the selected executable before\nrunning setup or store maintenance. The POSIX installer prints its absolute launcher because\nit cannot change the parent shell's command cache, aliases or functions. An older executable\nalready on disk remains unsafe for a newer store; do not use its force advice to recover.\n\nForward setup and connector seeding migrate built-ins automatically. Same-generation force and\nreset rebuilds remain supported. An executable older than the store now refuses every backward\nmigration, including force and reset. Select a verified executable at least as new as the store\nand run normal seeding instead. No credential or mesh-record format needs manual migration.\n\nA split deployment needs no broker restart for this change. Keep each host's CLI and connectors\non the same release. Pause new managed spawns while adopting it; a running manager keeps its\nloaded code until restarted. Use the chosen runtime's supported custody or preserve-state path.\nThe interruption covers that manager transition and depends on its runtime, not a broker outage.\nDo not overwrite a live manager installation or stop an unidentified listener to force adoption.\n\nAn implicit foreground spawn now chooses a numbered actor when the requested actor is occupied.\nExisting grants, launch material and sessions stay in place. An explicit name remains pinned\nand refuses an occupied actor rather than renaming it. Signed enrollments and managed handoffs\nkeep their existing identity semantics.\n\n## Background startup in 0.91.0\n\n`cotal up` now returns after background startup by default. `cotal up --detach` retains that\nmeaning. Use `cotal up --foreground` when terminal lifetime should own the native stack.\n\nA systemd unit, launchd KeepAlive job, container entrypoint or script that supervises the `up`\nprocess must add `--foreground` before upgrading.\n[Persisted callers](#persisted-callers-in-0910) gives the literal\nreplacement for each.\n\nA project folder an earlier release started keeps its own launcher after the upgrade. Stop it with\n`cotal down` as usual, upgrade, then run plain `cotal up` in that folder: it starts on the retained\nlauncher with the same identity, store and history. It does not need `space create` or `space add`.\n\nRecorded recovery requests retain their original foreground/background intent. They do not\nreinterpret a prior request using the new default. This change does not migrate a whole stack to\nowned services.\n\nPrepared plain-loopback user-auth roots can use the controller's broker, auth service, delivery and\nmanager members. First enable may return a known partial result after the first three start: the\nmanager needs an existing signed-in, granted CLI login. Sign in and obtain that grant, then run\nbare `cotal up`; no static caller or new grant is substituted. Auth endpoints come from the\ncreation-pinned child's native provider readiness, not a guessed URL. The controller keeps unknown\noperations blocked. Existing three-member auth/open journals are read with an unattempted auth row\nand written with all four roles; old clients do not understand that new record shape. Upgrade the\ncomposed CLI together before using it. TLS-required, remote and public-exchange user-auth paths\nretain their native route.\n\n## Persisted callers in 0.91.0\n\nSome command lines outlive the install that wrote them: OS units, scripts and container commands.\nAfter `npm i -g cotal-ai@<new>` they reach the new binary before any `cotal` command has run. This\nsection says what each one does after the upgrade and what to change before it.\n\n### Command mapping\n\n| Earlier command | Command in 0.91.0 |\n|---|---|\n| `meshes`, `meshes add` | `space`, `space add <name>` |\n| `clean store`, `clean all` | `reset store <space>`, `reset space <space>` |\n| `mint <name>` | `auth mint <name>` |\n| `mint --signer` | `auth signer --out <private-file>` |\n| `actor list`, `actor grant <actor>`, `actor revoke <actor>` | `auth grants`, `auth grant <actor>`, `auth revoke <actor>` |\n| `join [channel]` | `chat [channel]` |\n| `console` | `watch` |\n| `channels` | `channel` |\n| `send dm <agent>`, `send msg <channel>`, `send ask <role>` | `send -t <agent>`, `send -c <channel>`, `send -r <role>` |\n| `personas` | `persona` |\n| `endpoints`, `describe`, `invoke` | `endpoint`, `endpoint describe <target>`, `endpoint invoke <target> <command>` |\n| `deregister-instance` | `endpoint deregister <class> <instance-id>` |\n| `reconcile-gate`, `seats` | `manager reconcile`, `manager seats` |\n| `topology view` | `team check <file> --graph` |\n| `down -f <manifest>` | `team remove <deployment-id>` |\n| `status --components` | `service` |\n| `run journal`, `run migrate`, `run ps` | `run show`, `run check`, `run` |\n| `deliver pending <name>` | `pending <agent>` |\n| `web` | `service add <name> --preset web`, then `service start <name>` |\n| The four daemon entries | Their service presets, under [deprecated daemon entries](#deprecated-daemon-entries) |\n\nThe sections on this page for each command family give the details of its change.\n\n### What stops working\n\n`cotal up` returns after background startup by default. An earlier line such as\n`cotal up --space <space> --server <url>` that a supervisor runs as its main process now exits\nonce the stack is started. systemd then treats a `Type=simple` unit as finished and can stop the\nprocesses that unit started, and a restart policy starts another attempt. A launchd `KeepAlive`\njob relaunches it the same way. Add `--foreground` before upgrading. For a systemd unit, replace\n`ExecStart=/usr/bin/cotal up --space <space> --server <url>` with:\n\n```ini\n[Service]\nType=simple\nExecStart=/usr/bin/cotal up --foreground --space <space> --server <url>\n```\n\nFor a launchd job, add `--foreground` after `up` in `ProgramArguments`:\n\n```xml\n<key>ProgramArguments</key>\n<array>\n <string>/usr/local/bin/cotal</string>\n <string>up</string>\n <string>--foreground</string>\n <string>--space</string>\n <string>main</string>\n <string>--server</string>\n <string>nats://127.0.0.1:4222</string>\n</array>\n```\n\nA container command or script makes the same change to its `up` line. When background `up` is the\nmain process of a systemd unit, it prints a warning naming the unit and the `--foreground` remedy\nafter it starts or refreshes the stack. `up -f`, which has no foreground form, and a dry run print\nnothing. A launchd job gets no warning. `cotal up --detach` keeps its meaning. The warning changes\nno mode and no unit, and no unit or script is rewritten or restarted.\n\n`cotal spawn <name>` now launches through a manager and attaches. A container or script that ran\nan agent directly with `spawn <name>` adds `--foreground`.\n\n### Deprecated daemon entries\n\n`supervise`, `deliver`, `auth-service` and `feedback-intake` stay accepted with their existing\nflags for every caller: units Cotal installed, units and scripts you wrote, and container commands.\nThey are deprecated and are removed in `1.0.0`, one release for all four. Help and completion no\nlonger list them. Each run prints one line on stderr naming its replacement and that release.\nStdout, behavior and exit codes are unchanged, so no unit restarts in a loop because of the\nupgrade. `deliver pending` warns the same way and names `cotal pending <agent>`.\n\n| Entry | Replacement |\n|---|---|\n| `supervise` | `cotal service add <name> --preset manager` |\n| `deliver` | `cotal service add <name> --preset delivery` |\n| `auth-service` | `cotal service add <name> --preset auth-service` |\n| `feedback-intake` | `cotal service add <name> --preset feedback-intake` |\n\n`cotal service add <name> --preset <preset> --help` lists the options a preset takes, and\n`cotal service start <name>` starts it. A `supervise --roster <file>` line maps to the manager\npreset's `--roster <file>`, with an absolute path, and `cotal service start <name> --foreground`\nkeeps the manager in the foreground as `supervise` did. The manager, delivery daemon and auth service that `up`\nstarts itself do not warn, so the line in a log always comes from a unit, script or container you\ncan change.\n\n### Owned units\n\nA manager unit that an earlier `cotal service install` wrote keeps calling `supervise`. After the\nupgrade it starts the new binary and prints the deprecation warning on each start. This release\ndoes not rewrite that unit.\n\nA unit that this release's `cotal service install <name>` writes runs the hidden `__service-unit`\nentry, which an earlier binary does not have. Before you downgrade, stop it with\n`cotal service stop <name>` and remove it with `cotal service uninstall <name>`. After the\ndowngrade, install the unit again with the earlier binary's `service install`.\n\n### Containers\n\nThe `solo` service in `deploy/docker/compose.yaml` runs `spawn scout --foreground`. The team\nservice and the image's default command still run `supervise` with `--roster`, so their logs show\nthe deprecation warning. An image you already built carries its own CLI and is unaffected until\nyou rebuild it.\n\n### Before the upgrade\n\n- Add `--foreground` to every unit, launchd job, container command or script that supervises\n `cotal up`.\n- Add `--foreground` to every container command or script that runs `cotal spawn <name>` directly.\n- Move units and scripts that call `supervise`, `deliver`, `auth-service` or `feedback-intake` to\n their service presets before `1.0.0`.\n- Before a downgrade, stop and uninstall every unit this release's `service install` wrote.\n\n## Team commands in 0.91.0\n\nThe new grammar names fresh intent `team boot <file>` and additive intent\n`team deploy <file>`. `team check <file> --graph` resolves declared access\nwithout a broker. `team` and `team show <id>` read local fresh summaries, additive ledgers and\nagent/channel returned receipts; `team remove <full-id>` is the ID-only additive\nteardown name. An edited manifest is not authority to remove an old deployment.\n\nBoot's `--dry-run` is a declaration view.\nDeploy's `--dry-run` reads native presence and channel registry for advisory\nclassification against a prior ledger. Remove's preview compares a legacy\nledger with native manager PS and display-only membership observations, or a\nreturned channel-only receipt with current KV revisions. No preview authorizes\na launch or stop. Bare `team boot <file>` enters guarded native fresh\nstartup only for a prepared, stopped local root; it refuses a leftover manager\nrecord and offers rich, JSON and plain receipts. User-auth boot records an intent before\nowner/trust preparation and retains uncertainty without retry. Windows boot\napply refuses, because protected service-record inventory is not supported there.\nBare `team deploy` applies all-new channel-only manifests with explicit live\ndelivery, or one to 100 all-new agents on a static/open mesh, optionally with\nup to 100 new live channel cards. A mixed deployment records one intent before\ncreating channels and launching agents through one pinned manager/runtime.\nEach channel revision and original launch request, allocation and matched join\nis recorded before continuing. A full receipt requires both sequences to match.\nUnknown effects are retained, not replayed; completed effects are not rolled back.\nBoth routes offer rich, JSON and plain output. Identical settled deployments can return a verified\nno-op without new effects. Named stale-agent waivers retain the old policy without restarting.\nUser-auth agent apply and existing-card mutation still refuse effects.\n\n`team remove <id>` can retire a returned static agent deployment through its original manager.\nReview `--dry-run` first; scripts require `--yes`. This needs the native slot query, original UIDs\nand native retired slot/head plus cleanup evidence, not just stop acknowledgements. A returned\nchannel-only deployment instead uses deletes conditioned on its original card revisions. A static\nmixed deployment retires agents first and needs a complete retirement receipt before checking and\ndeleting cards, under one confirmation and maintenance lock. Both receipts must exist before its\nblocker is released. Partial and unknown effects retain their journals without replay. History,\ngrants and readers are unchanged. Open/user-auth agent retirement and legacy removal still refuse.\nThe old `spawn -f` manifest spelling no longer deploys. Native `up -f` still\nstarts a fresh mesh; `down -f <file> --run <id>` remains the retained route\nonly for an older additive ledger. Move an automated agent deploy or teardown\nto the new spelling only when its auth mode and manifest are ones these routes accept.\n\n## Credential commands in 0.91.0\n\nCredential minting is `cotal auth mint <name>`. The old `cotal mint` spelling refuses before\nissuance and names its replacement. A stripped account signer is exported with the explicit\n`cotal auth signer --out <private-file>`, not `mint --signer`.\n\nThe default standing credential label is create-only. Repeated minting does not silently replace\nit. Choose a new label or a separate private output. An explicit existing output is replaced only\nwhen it is an owned private native credential matching the supplied `--identity`. Explicit\nexports cannot overwrite managed `.cotal` or workstation state.\n\nThe new receipt reports the actual principal, resulting lifecycle, private destination and\nnative expiry without credentials. Reusing an nkey does not promise reuse of its old lifecycle\nor bind-only inboxes. An agent-profile re-mint reports its newly allocated UID and warns about\nthat distinction; consuming clients must use the returned UID. Provisioning is explicit and is\nnot evidence that a client joined. Scripts should parse the finite `--json` document rather than\nscraping the former text lines. Secret and signer files must stay out of support bundles.\n\n`auth mint <name> --endpoint <class> --out <private-file>` is a separate caller mode. It uses the\nsame native static authority and a live digest-verified untargeted synchronous ephemeral surface.\nTargeted, journaled or action commands refuse the whole issuance; no unsupported row is dropped.\nThe native agent footprint and exact additional endpoint rows are provisioned with required TLS,\nwithout adding spawn/run/admin scope or starting a seat. `--channels` enters both explicit channel\nACLs and defaults empty; `--expires-in` remains native seconds.\n\nThe create-only private `<out>.issuance.json` intent is retained before footprint work and contains\npublic identity/lifecycle/scope metadata only. It is not a native grant or completion receipt. An\nexisting output or intent blocks replay, including when an interrupted operation left no output.\nKeep that intent with any original file while diagnosing an unknown result. Provisioner close is\nreported separately. No raw-file caller identity, managed launch, user-auth issuer or environment-\nexport onboarding is added by writing this credential.\n\n## Endpoint deregistration in 0.91.0\n\nReplace `deregister-instance --endpoint <class> --instance <id>` with\n`endpoint deregister <class> <full-instance-id>`. Both positional values are required.\nThe old spelling refuses before effects and has no implicit instance selection. Review the\nnew `--dry-run` output, then use `--yes` in scripts. JSON retains native revision receipts;\nunknown or superseded deletions are not retried. User-auth and open targets do not receive\na static-signing fallback.\n\n## DM pending inspection in 0.91.0\n\nUse `cotal pending <agent>` for a one-document JSON/plain read of native DM\ndurable counts and bounded candidate IDs. `--durable <exact-dm-durable>` targets\nan older lifecycle directly. The retained `deliver pending` spelling still\nuses its earlier output shape; scripts must migrate to `result` fields rather\nthan assuming the old per-line format. Candidate IDs are not proof of\nrecipient delivery or complete message history. This route has only the\nstatic/open admin read authority; user-auth has no corresponding view yet.\n\n## Live space deletion in 0.91.0\n\n`space forget <name>` changes only the local registration. It does not delete\nlive broker state. `space delete <name> --dry-run` now reads known first-party\nnative streams, bucket backing streams and presence entries on the registered\nbroker in open or static-auth mode. Static transfer-stream names are read under\nan additional exact-name grant on a second confirmed connection. This non-atomic\nread is not an apply plan or evidence that presence records are live. User-auth\ninspection and deletion remain unavailable. Bare deletion now requires a typed\nexact-name confirmation on a terminal or the exact positional name with `--yes`\nin a script. It rereads the native impact and checks the current target before\nsubmitting exact stream deletes, stopping on the first refused or unconfirmed\nreply with acknowledged and remaining names. A native post-read reports any\nsurvivor or newly observed transfer target without retrying. No process, registration or local\nfile is removed.\n\n## Stopped reset in 0.91.0\n\n`reset store <space>` and `reset space <space>` are the named replacements for\n`clean store` and `clean all`. `reset store --dry-run` provides a partial\nlocal-impact preview of the store path, recorded processes, maintenance journal\nand broker credless reachability. `reset store` requires confirmation,\nacquires the maintenance lock, rechecks its exact store identity, legacy\nprocesses and all recorded service instances for that space, and calls the\nexisting store-only cleanup. Any recorded supervisor blocks reset. It keeps trust and the\nregistration. TLS-required or TLS-first store targets refuse, because no trusted\nstopped-broker check covers them. `reset space` now routes a\nreviewed local identity reset through native clean-all. It requires Linux, an\nisolated locally started registration and plain-NATS loopback nonresponse.\nIts preview inventories provider-declared keys and native filesystem targets;\napply rechecks the scope under the maintenance lock and constrains each delete\nto that reviewed inventory. Trust is removed last, followed by the unchanged\neligible registration. Unknown effects are not replayed. Service definitions,\npersonas, packages, IdP logins and remote data stay. Unsupported providers,\nforeign material, TLS-first and non-Linux targets refuse.\n\n## Backup maintenance in 0.91.0\n\n`backup create` supports one-document `--json` and plain reports. The completion receipt follows\nnative attempt cleanup. A published artifact with unconfirmed cleanup is partial; retain its\npath and original attempt instead of repeating creation. `backup inspect` reads metadata and\n`backup verify` checks declared file sizes and hashes. Neither proves restoration or authority\ncontinuity, starts a broker or writes a staging tree.\n\nThe canonical stale pre-commit recovery is `backup recover <full-attempt-id>`. Healthy retained\nold-source cleanup is `backup cleanup-fallback <full-attempt-id>`. Review `--dry-run`, then use\n`--yes` in scripts. Neither flag bypasses owner, deadline, root, journal revision or store-identity\nchecks. Restore startup stays on its guarded existing route.\n\nA controller-owned stack cannot be preserved in 0.91.0. On a root with a stack journal,\n`down --preserve-state`, `backup create`, `backup restore` and `up --restore` refuse `UNSUPPORTED`\nbefore any effect. Stop such a stack with plain `cotal down`; it keeps its store, channels and\nhistory for the next `cotal up`. Preserve, backup and restore of a controller-owned stack return in\na later release. A root without a stack journal keeps its preserve, backup and restore.\n\n## Feedback input in 0.91.0\n\nUse a protected `feedback --key-file <path>` rather than placing an intake key on argv.\nThe old `--key` spelling refuses without sending or echoing its value. Public feedback still\nworks with a contact email and requires no environment setup. JSON/plain output is one public\nreceipt, not a copy of the submitted body. Unknown submission outcomes are not retried, and no\nredirect or alternate credential/intake is substituted.\n\n## Credential diagnosis in 0.91.0\n\n`doctor auth` uses JSON/plain reports and distinguishes local claims from live authorization.\nA plain diagnostic that encounters material requiring migration refuses without moving it.\nExplicit `--fix` retains the native static daemon repair, with separate migration/re-sign receipts\nand no user-auth signer fallback. A local re-sign is not daemon adoption or broker acceptance.\nUnhealthy, partial and unknown results retain their classified outcomes; scripts must read the\nnew result and error fields instead of scraping the old `auth: healthy` line. Never respawn from\na locally ordered older credential file alone.\n\n## Service adapter declarations in 0.91.0\n\nA local-process provider can expose the version-1 workstation service ABI alongside its existing\nprocess metadata. It declares a closed option schema, explicit foreground/multi-instance/log\ncapabilities and launch/readiness hooks. Re-add an extension after adding this ABI so its cached\nservice metadata and package fingerprints are recorded. An older descriptor is not imported to\nguess its schema. Plain process definitions keep their existing meaning.\n\nPresets are declarative registry contributions. Their option flags, help, completion and guided\nquestions derive from the adapter schema. Re-add a preset provider to populate its new cache;\nmissing or changed metadata refuses without importing code to guess the definition.\n\nThe top-level `feedback-intake` launcher is a [deprecated entry](#deprecated-daemon-entries). For a\nnew intake, save a definition with\n`service add <name> --preset feedback-intake --space <s> --root <absolute-root> --keys <private-keys.json> --creds <private-creds>`, then use `service start <name>` in that space. An explicit\n`--store` must be an existing private file. The default is private and instance-keyed. No broker\nor other service is started, no signer is minted and required TLS is preserved. Native startup\nreceipts are private, request-specific observations rather than native endpoint serving claims.\n\nA service instance pins its adapter name, package version and declaration fingerprint. A later\npackage update does not silently adopt a running or retained instance under a different adapter.\nStop the exact owned process and explicitly remove its stopped records before choosing changed\nadapter metadata. Logs remain private local files. Extension removal refuses live, foreign or\nunresolved service instances, including records outside the current mesh selection. None of\nthese declarations migrate legacy PID records or supply OS-unit ownership. Native manager\nimpact comes from its explicit adapter's attributed current read, not cached metadata.\n\nThe delivery implementation now registers its service adapter and legacy custody descriptor.\nCustom composition roots that want those inventory/control rows import `@cotal-ai/delivery`.\nThe full binary already composes it. CLI stop helpers share the workspace descriptor without an\nimplementation-to-implementation import. The `delivery` preset requires the recorded root and\nexisting scoped credential, keeps the native store/account/lease guards and does not adopt an\nexisting legacy daemon. Its hidden child entry is recognized by the same process-role predicate.\nThe old `deliver` machine-facing route remains for the stack callers that still use it.\n\nFresh static-auth and open `team boot` now use controller-owned stack members and accept\n`--foreground`. Their manager receives a digest-pinned manifest launch spec, with no additive\nownership ledger. A recorded stack can resume only after a completed whole-stack stop with its\nmanager never attempted, recorded broker inputs and each member's settled native evidence. Legacy\ncustody, prior team boots, supervisors and unresolved attempts still refuse. User-auth preparation\nuses ordinary `up --user-auth`, login and an explicit grant, then `down` before `team boot`. No\ncaller or owner grant is substituted.\n\nNew controller journals record the broker store path, storage cap request and bind host. A later\npreparation keeps those inputs and refuses differing explicit values. Older journals remain\nreadable. If their broker is stopped, restart requires the original store named with `--store-dir`\nand the original `--max-file-store` and `--host` values, if set. The store must already be a real\ndirectory. Do not omit the cap or host unless the original start set neither. That restart records\nthe inputs; run `down` before the stopped-stack team route. No store path is inferred from an\nabsent field, and bare restart never silently opens a default empty store.\n\nThe native manager status contract adds optional caller-scoped stop-impact metadata and advances\nthe cluster revision. Upgrade the composed manager and its compatible registration host together;\na host that does not accept the current contract must refuse registration. Older managers can\nomit the observation. Component status then says impact was not reported, rather than inferring\nthat seats survive. An observed policy does not migrate PID records, reserve a stop or confirm\ncleanup. The manager package adds a separate controller-only `manager-service` adapter and\n`manager` preset. Existing legacy manager records are not migrated or adopted. Update the\ncomposed packages and extension metadata together; an old cached declaration is not rewritten\nbehind a retained instance. Native stops require a fresh full-scope read and unchanged request-bound\npolicy, with native returned evidence plus original process exit. Missing terminal evidence retains\nits reservation and blocks replacement, without force kill. Cleanup remains not checked.\nThe direct service route currently refuses foreground mode. On an open mesh there is no\nauthenticated caller, so readiness and stop impact come from the pinned child's own request-bound\nlocal receipt. Its registration must equal the launch identity bound to that PID. Status shows it\nas a child-reported local receipt, not an attributed endpoint response. OS installation and\nnon-Linux protected process support are not available.\nRecorded systemd user units with an original invocation ID use that invocation's native journal\nfor finite/follow logs. Missing invocation evidence refuses instead of reading a stale direct-child\nfile. Native cursors and timestamps are retained; journal retention/access is not proof of complete\noutput or completed service work. No unit install/control support is implied by this read route.\n\nAuth providers may implement the optional read-only `preflightServer` hook. It checks public\nserver inputs without creating a directory, pin, key, grant or daemon. The controller's user-auth\nroute requires it before preparation; providers without it refuse rather than skipping validation.\n`prepareServer` remains authoritative and checks again before provisioning.\n\nThe auth package also declares an `auth-service` preset over its prepared native key projection.\nIt does not migrate or reclaim existing PID records. Its real child claims custody before opening\nplanes and publishes a private startup phase only after native initialization and signal handlers.\nThis route declares shared foreground control and still refuses remote broker and TLS-required\ntargets. The transport refusal is\nintentional, because not every native authority connection carries the registered transport\nrequirement. No plaintext substitution or new issuer is used.\n\nManager maintenance now supports finite JSON/plain reports. Gate repair and seat drain default\nto cancellation in a terminal and require `--yes` in scripts. Use `manager reconcile --dry-run`\nor `manager seats --drain --dry-run` first. Gate repair requires one registered root/space/broker\nand carries its TLS requirement through native liveness/eviction. It does not dial a default broker\nwhen that target is missing. An interrupted submitted repair or unresolved drain can return exit 3;\nconfirmed native results remain distinct from unconfirmed effects. No process is started.\n\n## Linear reports in 0.91.0\n\nUpdate the installed Linear extension with the matching CLI/workspace generation. Account add/show,\ninventory and direct call/resource/prompt JSON now use version-1 envelopes: read the native\ninventory from `result.inventory` and a native request reply from `result.reply`. Rich/plain reports\nbound displayed rows and lines without truncating JSON. MCP tool `isError` remains a result with\nexit 0 and a warning, not a work-success claim. Native not-executed refusals stay distinct from\nunknown submissions, and neither is replayed automatically.\n\nThe `linear` service preset runs the same native endpoint implementation from an explicit static-auth\nroot and existing account. It retains native registration, renewal and bounded deregistration; no\nnew generic caller is issued. Its private phase proves handler initialization only, not a current\nendpoint reply or completed deregistration. Foreground service start uses the shared terminal\nlifetime and native signal handlers, not the old serve parser. User-auth/open targets and\nOS-supervised service operation refuse. The older `linear serve` route remains available for\ninstalled callers.\n\nA bare Linear/account group opens no account. Wrong-mode flags or extra operands refuse before\naccount access. Interactive OAuth remains a human flow without JSON. Foreground serve stays on its native human-output path.\nStatic caller issuance keeps its native authority and now returns shared JSON/plain private-file\nreceipts. Use an existing protected physical parent outside managed state; an existing output\nrefuses before provisioning. Uncertain provisioning/publication is not replayed and no environment-\nexport launch recipe is printed. The supported `auth mint --endpoint` form resolves its scope live;\ntargeted/journaled/action issuance and consuming launch adapters are not supported. These changes do\nnot replace ownership, start a service or print secrets.\n\n## Setup inputs in 0.91.0\n\n`cotal setup` takes four inputs: destination, application, project folder and space. Nothing on a\nrunning mesh changes, and there is no outage window.\n\n### What stops working\n\n- A `cotal setup --yes` or a setup with no terminal exits 1 with `USAGE_INVALID` unless it names\n every input as a flag. It no longer sets up every detected connector and seeds the folder it\n runs in.\n- Setup sets up one application, the one `--harness` names, and records it as the space's\n `harness` preference. An application whose executable is not on PATH exits 4 with the rest\n saved; it used to be skipped with a warning.\n- The default persona lands in `<project>/.cotal/agents`. Setup no longer looks for a running mesh\n to seed into.\n- A repeat `cotal setup` no longer prints a status card. `cotal status` reports the same state.\n\n### Before the upgrade\n\nA script or image that ran `cotal setup --yes` names the four inputs:\n\n```bash\ncotal setup --destination local --harness claude --project <dir> --space <name> --no-input\n```\n\nFor a space that runs elsewhere, register it first with `cotal space add`, or pass the bundle its\noperator exported with `--connection-file`, and use `--destination self-hosted` with the\nregistration's root as `--project`.\n\nStartup remains opt-in. `--start` conflicts with `--configure-only`; a script, JSON or plain\ninvocation must add `--detach`. `--yes` by itself still saves configuration only. Startup failure\nleaves completed preparation in the result. Do not treat an unknown startup result as permission\nto submit another launch; inspect the selected space and recorded original request first.\n\n## Reads that install nothing in 0.91.0\n\nA read no longer installs or refreshes the built-in connectors and the web dashboard before it\nruns. Nothing on a running mesh changes, and there is no outage window.\n\n### What stops working\n\n- On a fresh machine, `cotal status`, `cotal ext list`, `cotal ps`, `cotal runtimes`, `cotal docs`\n and the other reads leave the extension root empty. `cotal setup`, or any command that is not a\n read, installs the built-ins as before.\n- After an upgrade, a read does not refresh the built-ins. Where it would load one still at the\n older version, such as a connector's setup row in `cotal status`, it names the package and its\n version with `cotal ext seed` instead of loading it.\n\n### Before the upgrade\n\nA script that ran a read to install the built-ins, such as `cotal ext list` right after\n`npm i -g cotal-ai`, runs `cotal ext seed` instead.\n\n## Space selection in 0.91.0\n\n`cotal use` and the target a command picks without `--space` change. Nothing on a running mesh\nchanges, and there is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal use <space>` no longer checks the broker. It selects any registered space, including a\n stopped one, and the command that targets it reports the space not up. A name that is not\n registered exits 2 with `TARGET_NOT_FOUND` instead of 1. Bare `cotal use` reports the selection\n instead of exiting 1 with its usage, and `--json` writes one envelope.\n- A command run without `--space` stays on the selected space when its broker is down and refuses\n with `TARGET_UNAVAILABLE`, exit 2. It used to fall back to the one other running mesh and print\n a note that the default mesh was down. A selection that names a space no longer registered now\n refuses with `TARGET_NOT_FOUND` instead of falling back to the local project or the one running\n mesh.\n- `cotal space` lists the registered spaces. `cotal meshes` still lists them, and its `--json`\n lines keep their shape.\n\n### Before the upgrade\n\nA script that relied on the fallback names the space it means with `--space`, or selects it with\n`cotal use <space>`. A script that read `cotal use` exiting 1 for a stopped space checks the\nbroker with `cotal space show <space> --json` and reads `result.check`.\n\n## Extension listing output in 0.91.0\n\n`cotal ext --json` and `cotal ext list --json` now write one JSON document instead of one JSON line\nper extension. Nothing on a running mesh changes, and there is no outage window. Upgrading the\n`cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ext list --json` line by line reads one envelope instead. The rows it\n read are now `result.items`, each with the same fields, and `result.root` is the install prefix.\n With `jq`, `cotal ext list --json | jq -c '.result.items[]'` prints the rows as before. An empty\n prefix writes an envelope with no items instead of nothing.\n- A usage error from `cotal ext` exits 1 as one `refused` envelope under `--json`, and `--plain` or\n `--json` on `ext add`, `ext remove`, `ext root` or `ext seed` is refused. `cotal ext root` still\n prints the path alone.\n- The human listing is a table now. Human output is not a parsing target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ext list --json` and `cotal ext --json`, and change line-by-line reads to\nread `result.items` from the one document.\n\n## History reads in 0.91.0\n\n`cotal history [channel]` reads retained messages. Clearing is now `cotal history clear`, which asks\nin a terminal and takes `--yes` in a script.\nThis changes the CLI grammar only. Stored messages, credentials, agents and running services do not\nmigrate or restart, and there is no outage window. Upgrade the CLI and its first-party components\ntogether through the normal update path; a separate broker or manager can stay running.\n\n### What stops working\n\n- Replace a script's `cotal history --force` with `cotal history clear --yes`. `--force` is refused\n on every `history` form, `history clear` included, rather than clearing anything. Add `--dms` to\n the explicit clear command only when the purge should include direct messages and the identity\n holds that authority.\n- `history clear` accepts no channel argument, read filters or `--follow`. It purges every channel\n in the selected space; these inputs are refused rather than treated as a selective purge.\n- Bare `history` reads channels within the caller's read grant. It never clears stored messages.\n Use `--json` and `result.items` for scripts; the human report is not a parsing target.\n- A user-auth history read uses the connection's channel grant. DM audit history and stream-sequence\n cursors are not available through that ordinary human credential. Use a time with `--since`.\n\n### Before the upgrade\n\nFind scripts that pass `--force` to `history` and change their explicit purge invocation. No stored\nstate or credential backup is needed for this CLI-only transition. Keep the deployment's usual\nbackup procedure before an intentional history purge; reads do not make a backup or restore data\nthat retention has already removed.\n\n## Status output in 0.91.0\n\n`cotal status` writes one structured document with `--json`, takes `--plain` and `--all`, and exits by\nthe structured outcome table. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal status` exits 4 when it could not read part of what it reports, such as a PID record, the\n folder's trust material, a persona catalog, the mesh registry or one of its records, or a user-auth login. It exited\n 0. An unreachable broker, a stopped process\n or an unbound responder still exits 0.\n- The recorded meshes list checks only the selected mesh and reads `not checked` for the others. Pass\n `--all` to check every one.\n- The report no longer lists the roster or prints a `cotal status` title line, and its standalone\n hints moved to a `Hints` section at the end. Rows are labeled `name: value`. Human output is not a\n parsing target; use `--json`.\n- `--components` is refused with `--json`, `--plain` or `--all`. Its exit, `0` to `3` by component\n health, replaces the report's, so it does not exit 4 for a partial report.\n\n### Before the upgrade\n\nSearch scripts and monitors for `cotal status`. A check that treats any nonzero exit of bare\n`status` as an outage should treat 4 as a report with unreadable parts. A script that read the\nroster from `status` should use `cotal ps` for managed seats.\n\n## Read command output in 0.91.0\n\n`cotal ps --json` now writes one JSON document instead of one JSON line per seat, and `ps`, `models`\nand `runtimes` exit by the structured outcome table. Nothing on a running mesh changes, and there is\nno outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ps --json` line by line reads one envelope instead. The rows it read\n are now `result.items`, each still the manager's row unchanged, and `result.managers` names each\n manager instance and whether it answered. With `jq`, `cotal ps --json | jq -c '.result.items[]'`\n prints the rows as before. Instance headers no longer go to stderr under `--json`.\n- An incomplete manager census, where a registered manager did not answer, refused or serves a\n different contract, exits 4 instead of 1.\n- A mesh that `ps` or `models` cannot resolve or reach exits 2 instead of 1, and a space catalog\n refresh that `ps` or `models` needed and could not get exits 2 instead of 1. Under `--json` each is\n one `refused` envelope whose `error.code` names the reason.\n- `cotal models` exits 4 when a connector's catalog could not be read, and `cotal runtimes` exits 4\n when an installed runtime provider fails to load. Both exited 0. `cotal models --agent <name>`\n exits 5 when that one catalog could not be read. OpenCode 2.x, which has no catalog the CLI can\n list, is reported `supported: false` with the reason in `unavailable` and does not fail the read.\n- The human reports of `ps`, `models` and `runtimes` are tables now, and `ps --wide` prints one\n labeled record per seat instead of a facts line under each row. Human output is not a parsing\n target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ps --json`, and for checks of the exit status of `ps`, `models` or\n`runtimes`. Change line-by-line reads of `ps --json` to read `result.items` from the one document.\n\n## Shell completion script in 0.91.0\n\n`cotal completion <shell>` is now `cotal completion script <shell>`, and bare `cotal completion`\nreports which shells have the stub instead of printing usage. Nothing on a running mesh changes, and\nthere is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\nA startup line written by hand that runs the old form, such as `source <(cotal completion bash)` or\nthe PowerShell profile line `cotal completion powershell | Out-String | Invoke-Expression`, now\nloads no completion. The command exits 1, prints nothing on stdout, so the shell sources nothing,\nand names the replacement on stderr. Replace the line with its new form:\n\n```bash\nsource <(cotal completion script bash) # bash\nsource <(cotal completion script zsh) # zsh\ncotal completion script fish | source # fish\ncotal completion script powershell | Out-String | Invoke-Expression # PowerShell $PROFILE\n```\n\nA cached stub from an earlier release keeps calling the hidden `__complete` entry. The new ownership-aware\ninstaller does not adopt it just because its text matches a generated script. Review unrecorded\nstubs and custom startup wiring manually before replacing them. Nothing automatically rewrites\na hand-edited startup file. On Linux and macOS, `completion install powershell` asks the\ninstalled shell for its current-user all-hosts profile before adding an owned block;\nit does not adopt an existing manual line. Windows profile editing still requires a\nmanual review and the printed script.\n\nNew installs record their stub hash and startup block. Reinstall and update refresh unchanged\nrecorded stubs at the same path. Update adds no startup block and does not restore a stub the user\nremoved. Uninstall removes only unchanged recorded material. Modified or unowned material is\npreserved with a diagnosis. Reopening the shell is necessary to stop using functions already\nloaded in its parent process.\n\n## CLI command resolution in 0.91.0\n\nThe CLI now resolves and validates a command before it seeds connectors or writes configuration.\nNothing on a running mesh changes. Brokers, managers, seats, credentials and mesh records need no\nmigration and no restart, and there is no outage window. Upgrading the `cotal` binary is the whole\noperation. What changes is what two kinds of command line do.\n\n### What stops working\n\n- `cotal -v` no longer prints the version. It exits 1 and names `cotal version` and `cotal -V`. A\n script that reads the version this way must use `cotal version`, `cotal --version` or `cotal -V`.\n- The words `sp`, `a`, `s`, `r`, `x` and `h` are now aliases of built-in commands (`spawn`,\n `attach`, `status`, `run`, `ext` and `history`). An installed extension that provides a command\n under one of those words still loads, but the built-in wins and every run prints a warning naming\n the extension. Remove or update that extension to clear it. `cotal ext add` refuses a new\n extension whose name or alias is already claimed.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal -v`, and run `cotal ext list` to see whether an\ninstalled extension provides one of the words above. There is nothing to snapshot. The extension\nmanifest an older binary wrote is read as before.\n\n## Messaging commands in 0.91.0\n\n`cotal channels` is now `cotal channel`, and `cotal send` names its destination with a flag instead\nof a `dm`, `msg` or `ask` word. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal channels`, with or without `list`, `set` or `default`, exits 1 as an unknown command. Bare\n `cotal channel` lists the channels, `cotal channel set <name>` saves one channel's settings, and\n `cotal channel defaults` reads or updates the space-wide defaults.\n- `cotal send dm <agent> \"<text>\"`, `cotal send msg <channel> \"<text>\"` and\n `cotal send ask <role> \"<text>\"` exit 1 with `USAGE_INVALID`, because they name no destination\n flag. Use `cotal send -t <agent> \"<text>\"`, `cotal send -c <channel> \"<text>\"` and\n `cotal send -r <role> \"<text>\"`.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal channels` and `cotal send`, and change each line to the\nnew form above.\n\n## Managed Claude workspace trust in 0.80.0\n\nBefore upgrading, snapshot the manager's private Cotal state and Claude configuration home.\nPause new managed spawns while upgrading the connector, core, manager and chosen terminal\nruntime together. For tmux, cmux, Herdr, Orca and custodial Linux PTY seats, use that runtime's\nsupported manager-reload path and observe its custody/release bounds. Default `auto` selects\nin-process `pty`, whose seats cannot survive a manager restart. Drain those seats at an approved\nidle window, or use the documented preserve-state cut and `up` recovery, which resumes them\nrather than keeping their processes running. This upgrade does not require a broker restart.\n\nManaged launches now accept the authorized workspace through Claude's native startup dialog. They\nno longer require a prior interactive visit to that directory. Claude retains its normal\nrepository and linked-worktree trust semantics, including any trust it persists for later\nsessions. Foreground launches still prompt. No tool-permission bypass flag is added, but\naccepting workspace trust enables that checkout's hooks and settings, including its default\npermission mode unless a launch flag overrides it. Authorize only reviewed workspaces.\n\nThe launch recipe includes optional startup choices before its final confirmation. A custom\nruntime must implement `LaunchSpec.confirmBefore`, or refuse a recipe carrying it. Ignoring\nit leaves an untrusted launch at the native dialog until startup times out. The new launch\nrecipe does not require restarting an existing seat; manager reload continuity remains\nruntime-dependent as described above. Adopt the new runtime before launching new seats.\nNo credentials, mesh records or operator configuration files need manual migration.\n\n## AG-UI `takeCodePoints` export in 0.79.0\n\n`@cotal-ai/connector-core` no longer exports `takeCodePoints`. The AG-UI preview splitter is its\nonly user, and the package exported it only so the emitter could shorten a `RUN_ERROR` failure\ndetail, a path that never ran and is now removed. Nothing about a running mesh changes, and no\nshipped connector or host imports it. Only code of your own that imports it from the package is\naffected.\n\n### What stops working\n\nAn import of `takeCodePoints` from `@cotal-ai/connector-core` no longer compiles, and an ES module\nthat names it fails to link with `does not provide an export named 'takeCodePoints'`.\n\n### Before the upgrade\n\nTake a string's first `n` code points with `Array.from(s).slice(0, n).join(\"\")`, which is what the\nhelper did.\n\n## SecretStore create-only write in 0.78.0\n\n`SecretStore` from `@cotal-ai/core` has a fourth method, `create(key, value)`. It stores the value\nonly while the key is absent and resolves `true`, or resolves `false` and leaves the stored value in\nplace. The auth secret kinds made once per space now write their first value through it: the callout\naccount, the issuer keys, the owner secret and a hosted auth plane's instance identity. Before, two\nfirst calls on one space each made a value, the later `put` replaced the earlier one, and the earlier\ncaller kept using a value the store no longer held. The filesystem store implements `create` with an\nexclusive create, so a CLI root needs nothing, and stored values do not change.\n\n### What stops working\n\nA store of your own without `create` no longer compiles as a `SecretStore`. Plain JavaScript gets a\n`TypeError` at the first call that makes one of those kinds, such as `prepareServer` on a new space\nor the first `startAuthService` of a hosted context.\n\n### Before the upgrade\n\nAdd `create` to your store, decided in one atomic step by the backend, such as a conditional create\nor an insert that fails on an existing key. A read followed by a write brings the race back.\n\n## Manager-service authority refusal budgets in 0.78.0\n\n`POST /manager-service-authority` now counts its refusals against the exchange face's budgets, as\n`POST /exchange` does. On the loopback face a missing or wrong capability counts in the\ninvalid-capability window, and a refused IdP token or request counts in the refused-exchange window.\nOn the public face a refusal counts in the peer's bucket. A running mesh needs nothing, and a\nrequest that succeeds is never throttled.\n\n### What stops working\n\n`ManagerServiceAuthorityCtx` from `@cotal-ai/auth` now requires `failures` and `badCaps`, so a host\nthat builds it in an object literal without them fails to compile. The handler now calls the\n`peerKey`, `throttled` and `recordFailure` of the policy it is given. After 30 refusals in a minute\non one face, a further refusal on either route answers 429 without its reason.\n\n### Before the upgrade\n\nPass the `failures` and `badCaps` arrays your `/exchange` route counts in, so both routes share one\nbudget per face. A host that serves only this route can pass new empty arrays.\n\n## Primitive scope kind in 0.78.0\n\n`PrimitiveSpec` from `@cotal-ai/lang` replaces `opensScope: boolean` with `scope: ScopeKind | null`,\nthe scope kind a primitive opens, in the way `kind` names the effect it journals. Both engines now\nread that kind from the table. Before, they cast the primitive's name to `ScopeKind`, so a scope row\nmissing from the union compiled and reached the journal as a kind its readers did not know. A\nrunning mesh and its journals need nothing.\n\n### What stops working\n\nCode that reads `opensScope` from `PRIMITIVES` or a `PrimitiveSpec` fails to compile, and plain\nJavaScript reads `undefined` there, so a check on it never holds.\n\n### Before the upgrade\n\nRead `spec.scope !== null` where you read `spec.opensScope`, or `spec.scope` when you need the kind.\n\n## Token verifier clock tolerance in 0.77.0\n\n`validateUserToken`, and the `IdpConfig` that `verifyIdpToken` and `createIdpBridge` take, from\n`@cotal-ai/auth` no longer accept `clockToleranceSec`. Both verifiers allow a fixed 5 seconds of\nclock skew on `exp`, `nbf` and `iat`, the default they already applied. Nothing in the repository\npassed the option, so a running mesh behaves as before. Only code of your own that passes it is\naffected.\n\n### What stops working\n\nA call or `IdpConfig` that passes `clockToleranceSec` in an object literal no longer compiles. Plain\nJavaScript that keeps it still runs, and the value is ignored. A `NaN` value, such as `Number()` of\nan unset environment variable, used to turn off the expiry, not-before and issued-at checks in both\nverifiers, so an expired or post-dated token was accepted. A large value widened those checks by\nthat many seconds. Both kinds of token are now refused.\n\n### Before the upgrade\n\nRemove `clockToleranceSec` from each call and each `IdpConfig`. A host whose clock is more than 5\nseconds off the issuer's needs its time synchronized.\n\n## Auth context closure in 0.71.0 (unreleased)\n\nExisting deployments need no credential migration or restart for these additive APIs. Embedded\nhosts can now inspect `handle.connections()` and await `handle.closed` after `close()` or `drain()`\nto prove every owned transport ended, including the callout, replaced readiness readers and\nshort-lived clients. The inventory is a detached snapshot.\n\nA transport close failure now rejects with its connection label. The terminal signal stays pending\nwhile any connection remains live. Repair the failure and retry `close()` before awaiting\n`handle.closed`. Closing one hosted context does not close another account's context.\n\nRead a space's claim with `readPlaneClaim(kv, space)` on that account's leader-only auth bucket.\nAn unclaimed space returns `undefined`; held and released rows retain their claim identity.\nDeleted, malformed and foreign-space rows refuse. `PlaneClaimRow` and `PLANE_CLAIM_KEY` are exported.\n\nUse `observeAccountLivenessWithCreds({ servers, observerCreds, accountId, options })` with the\naccount-scoped membership-observer credential to list that account's connections. It never widens\ncredentials or evicts connections. Zero rows prove absence only with a complete sweep and the\nsingle-server proof. An embedded endpoint's trusted composition can retain transport custody\nthrough `EndpointOptions.onConnection`.\n\n## Unreleased\n\nOn a per-user-auth mesh, a spawn-scoped caller can arm the event plane of a child under its own\nowner without `admin`. This fixes owned spawns in spaces whose registration policy requires the\nplane. Upgrade the manager to pick up the admission change. No credential or state migration is\nneeded. Cross-owner arming still requires `admin`, and the child's own-channel rule and ledger\nenvelope are unchanged. A silent non-owner caller in a space without the policy still has the\nplane disarmed, with a notice if provisioning succeeds.\n\n## Hosted auth plane identity in the store (unreleased)\n\nA hosted context started through `startAuthService` keeps its auth plane instance identity in the\ninjected `SecretStore` under `authInstanceKey(space)`, with the other auth secret kinds. It used to\nsit under `stateDir` at `.cotal/space.<hex>/auth-instance.json`, though `stateDir` holds non-secret\nstate and the record holds the plane's private serve seed. The first start of an upgraded context\nputs that record into the store, removes the file and keeps the instance. A CLI root is unchanged.\n\n### What stops working\n\nA start refuses when the store and `stateDir` hold different instance identities, and names both. A\nstore that refuses a put of the new key fails the start.\n\n### Before the upgrade\n\nLet the store accept a put of `authInstanceKey(space)`. A copy of `stateDir` taken before the upgrade\nstill holds the serve seed, so delete it or protect it as secret material.\n\n## Injected SecretStore naming in 0.76.0\n\nThe manager and the delivery daemon name an injected `SecretStore` through\n`injectedSecretStoreIdentity` from `@cotal-ai/core`. The rule is unchanged: the store's declared\nidentity, else the coordinate in `COTAL_SECRET_STORE`, else a refusal. A running mesh needs nothing.\n\n### What stops working\n\n`reloadStoreIdentityOf` from `@cotal-ai/delivery` takes `{ injected: true, store }` or\n`{ injected: false, identity }`. A call that passes `injected: true` with no store fails to compile,\nand plain JavaScript gets a `TypeError`. Both processes refuse an unnamed injected store with one\nmessage, which starts `an injected SecretStore must declare its identity`, so a log match on either\nold message no longer matches.\n\n### Before the upgrade\n\nPass the injected store to `reloadStoreIdentityOf`, or call `injectedSecretStoreIdentity(store)`.\n\n## Manager-service authority policy flag in 0.75.0\n\n`handleManagerServiceAuthority` from `@cotal-ai/auth` no longer reads `allowManagerAuthority` from\nits policy argument, and the field is removed. Both exchange faces set it to `true`, so it never\nrefused anything. A running mesh needs nothing: the public listener serves\n`POST /manager-service-authority` as it did before, and the docs now list that route.\n\n### What stops working\n\nA host that serves the route itself and passes a policy object literal with\n`allowManagerAuthority` fails to compile with TS2353. Plain JavaScript that set it to `false` got a\n403 from the handler, and its requests now reach the capability and IdP checks.\n\n### Before the upgrade\n\nRemove `allowManagerAuthority` from the policy you pass. Where you set it to `false`, leave the\nroute out of that listener's route table instead.\n\n## Hermes model from the environment in 0.68.0\n\nA connector now launches on the model and variant its launcher resolved (the `--model` or\n`--variant` flag, else the agent file's `model:` or `variant:`) and no longer reads them again from\nthe agent file. The Hermes connector also no longer takes a model from `HERMES_MODEL` in the\nenvironment of the process that spawns the seat, including when `spawn.env` lists it.\n\n### What stops working\n\nA Hermes spawn whose only model was `HERMES_MODEL` in the spawning environment is refused at launch,\nand the refusal names both ways to set a model. Spawns that set `--model` or `model:` are unchanged,\non every connector.\n\nCode that calls a connector's `buildLaunch` directly with only `configPath` now gets no model or\nvariant from that file. Pass them as `model` and `variant`.\n\n### Before the upgrade\n\nMove each Hermes seat's model from `HERMES_MODEL` onto its spawn with `--model`, or into its\npersona's `model:`.\n\n## Run answers on a participant manager in 0.68.0\n\nA participant manager now asks its issuing host for an answering credential by naming the run and\nstep it answers. The host reads the pause's token off that run's journal and no longer accepts a\ntoken from the manager. Runs on a mesh with no participant manager are unaffected.\n\n### What stops working\n\nWhile a participant manager and its issuing host run different sides of this release, the host\nrefuses every `cotal run answer` and every amendment that manager serves, because each side refuses\nthe other's request shape. Starting, resuming and reading runs is unchanged. A pause stays waiting\nthrough the window, or follows its timeout if it has one.\n\n### Before the upgrade\n\nUpgrade the auth service and every participant manager registered with it in the same window, then\nanswer the pauses that waited.\n\n## Headless OpenCode handshake in 0.69.0\n\nWith `COTAL_SERVE_HEADLESS=1`, the OpenCode launcher's `[cotal-serve]` line on stdout now carries\nonly `port` and `session`. The server password no longer appears in it, and the 1.x TUI no longer\nreceives the password on its command line.\n\n### What stops working\n\nA headless host that read `password` from that line has no password, and the server refuses its\nrequests. Seats with a TUI, and headless seats that no host drives, are unaffected.\n\n### Before the upgrade\n\nHave each headless host mint a password and pass it to the launcher as `OPENCODE_SERVER_PASSWORD`,\nthen use it for basic auth as before. Without that variable the launcher mints its own.\n\n## Filesystem store identity in 0.69.0\n\nThe delivery daemon's answer to the manager's store check now names a filesystem store by its root\nand by a random `id` that the store records once in `store.id` inside its own directory:\n`.cotal/store.id` for a workspace root, or the directory of the file for `cotal deliver --creds\n<file>`. A manager no longer counts the daemon's store as its own because the two roots have the\nsame path. On a split whose broker host and manager host use one root path, the manager host now\nstays off the daemon-credential renewal lease, so `cotal doctor auth --fix` on the broker host can\nrenew the daemon credentials.\n\n### What stops working\n\nA manager and a delivery daemon on different sides of this release refuse each other's answer to\nthe store check. The manager then remints no daemon credential, and a manager that is booting does\nnot start. This is read from the code and was not measured across two releases. A\n`cotal deliver --creds <file>` whose directory is a read-only mount and holds no `store.id` stops at\nstart. So does a `--creds` file that is its directory's `store.id` under any name, and a `store.id`\nthat is a symbolic link or holds anything but a lowercase UUID.\n\n### Before the upgrade\n\nUpgrade the broker host and every manager host of a space in the same window. For a `--creds` file\non a read-only mount, add a regular `store.id` file beside it that holds a new lowercase UUID and no newline,\nas `node -e 'process.stdout.write(crypto.randomUUID())' > store.id` writes. Move a `--creds` file\nnamed or linked as `store.id` to a file of its own.\n\n## Detached spawns with `--share-tools` in 0.69.0\n\nThe manager's `spawn` operation now takes `shareTools` as a list of MCP server names. The CLI parses\n`--share-tools` into that list before it sends the request, and the manager cluster document moves\nto revision 22. A cut taken with `cotal down --preserve-state` before the upgrade still resumes: the\nmanager reads its `cotal-manager-resume/v1` inventory and writes new cuts as\n`cotal-manager-resume/v2`.\n\n### What stops working\n\nA CLI and a manager on different sides of this release refuse a detached spawn that passes\n`--share-tools`, because the CLI checks each request against the contract the manager serves. This\nis read from the code and was not measured across two releases. A detached spawn without the flag,\na foreground spawn and a roster entry are unaffected. A manager older than this release cannot\nresume a cut that this release took.\n\n### Before the upgrade\n\nUpgrade the CLI on every host that runs `cotal spawn --detach` in the same window as the managers\nit reaches.\n\n## Shared MCP server checks in 0.69.0\n\nThe cotal config reader now checks each server under `connectors.<name>.mcpServers` when it reads\nthe file, and refuses one that cannot launch as written, naming the file and the field. The rules\nare in [the config file](config.md#the-config-file).\n\n### What stops working\n\nA config file that holds such a server refuses every Claude spawn that reads it, including one with\n`--share-tools none`. Before, a field of the wrong type failed each Claude spawn that shared the\nserver with a `TypeError` that named neither the file nor the server, a spawn that did not share it\nlaunched, and a server with no `command` or `url` was passed to `claude`, which never started it.\nRead from the code and not measured: spawns on other connectors, a manager resume and the step of\n`cotal setup` that records the shared list read the same files, so each stops at the same refusal.\n\n### Before the upgrade\n\nCheck `connectors.<name>.mcpServers` in the operator-level config file and in each space's\n`.cotal/config.json`. Give each server a string `command`, or a `type` of `http`, `sse` or `ws` with\na string `url`. Write `args` as a list of strings and `env` and `headers` as objects of strings, or\nremove the server.\n\n## Remote manager family eviction in 0.69.0\n\nA remote manager registered through its host now asks the host to evict up to 256 holders of its\ncredential family in one maintenance request, and the host reads the family once for the whole set.\nBefore, a restart sent one request per holder and the host read the whole family for each one.\nMeshes with no remote manager are unaffected.\n\n### What stops working\n\nWhile a remote manager and its issuing host run different sides of this release, each side refuses\nthe other's eviction request shape. A restart whose credential family already has holders then fails\nat its eviction step and leaves the manager's registration gate frozen. A first start, a clean stop\nand the host's reconciliation of a foreign slot holder are unchanged.\n\n### Before the upgrade\n\nUpgrade the auth service and every remote manager registered with it in the same window. A manager\nthat restarted inside the window resumes its frozen registration on its next start once both sides\nrun this release.\n\n## AG-UI emitter holder hooks in 0.69.0\n\n`AguiEmitterHolder` from `@cotal-ai/connector-core` now takes its hooks as one named object after\nthe emitter factory: `new AguiEmitterHolder(startEmitter, { onError, onRunClosed, waitLive, runMeta })`.\nOnly `onError` is required. Nothing about a running mesh changes, and every shipped connector passes\nits hooks by name. Only a connector of your own that builds a holder is affected.\n\n### What stops working\n\nA holder built with positional hooks, such as `new AguiEmitterHolder(start, onError, onRunClosed)`,\nno longer compiles, because the constructor takes two arguments. Plain JavaScript that keeps the\npositional form still runs, but the holder calls none of its hooks, so a failure never reaches\n`onError`.\n\n### Before the upgrade\n\nPass each hook by name, for example `new AguiEmitterHolder(start, { onError, onRunClosed })`, and\ndrop any `undefined` that filled an earlier slot to reach a later hook.\n\n## Worker run failure type in 0.70.0\n\n`WorkerRunFailed`, the failed result of `runInWorker` in `@cotal-ai/lang`, is now a union on\n`class`: `released`, `held`, `effect`, `too-large`, `rejected` or `error`. A running mesh needs\nnothing, because the runtime host and the engine thread ship in the same install. A run on the\ncompiled engine whose program throws an object with `code: \"L5012\"` or `code: \"L5025\"` used to end\nreleased and now ends failed, as it does on the walker.\n\n### What stops working\n\nTypeScript code that reads `code`, `reason`, `step`, `pending`, `kind`, `detail` or `tooLarge` on a\n`WorkerRunFailed` it has not narrowed fails with TS2339. A released, held, too-large or rejected\nresult no longer carries `code`, so JavaScript that branched on `L5012`, `L5025`, `L5006` or\n`L5010` stops matching with no error. `tooLarge` is gone.\n\n### Before the upgrade\n\nBranch on `class` where such code read `code`: `released` for L5012, `held` for L5025, `too-large`\nfor L5006 and `rejected` for L5010. An `effect` or `error` result keeps its `code`. Once narrowed to\n`too-large`, a result carries the `stepKey`, `bytes` and `bound` that `tooLarge` held.\n\n## Remote manager request builder in 0.70.0\n\n`remoteManagerClient.remoteManagerAuthorityRequest` from `@cotal-ai/manager` now takes an\noperation's coordinates as one object, and `remoteManagerRegistrationProof` from `@cotal-ai/core`\ncomputes the proof from the manager's identity state instead of a request. Nothing about a running\nmesh changes: the proof digest and the request on the wire are the same, so a manager and a host on\ndifferent sides of this release still accept each other. Only code that builds remote manager\nrequests itself is affected, in TypeScript and in plain JavaScript.\n\n### What stops working\n\nA call that passes the registration proof, contract artifacts, session, retirement or transfer\nreader as positional arguments after the operation no longer compiles. A call that passes a request\nto `remoteManagerRegistrationProof` no longer compiles either, because the second argument now names\nthe lifecycle `lifecycleUid`, as the identity state does.\n\nPlain JavaScript runs both old calls without an error. The builder drops the positional coordinates,\nso the host refuses the request with `requires a sha256 registrationProof`. A proof computed from a\nrequest leaves out the lifecycle, so the host refuses a request that carries it as a proof mismatch.\n\n### Before the upgrade\n\nName the coordinates, for example\n`remoteManagerAuthorityRequest(state, \"cli\", \"retire\", { registrationProof, retirement })`.\nCompute the proof as `remoteManagerRegistrationProof(owner, state)`, adding the contract artifacts\nas a third argument for activation only. A host that recomputes the proof from a received request\npasses `{ space, instanceId, lifecycleUid: managerLifecycleUid, identities }` from that request.\n\n## Bearer validator lifetime cap in 0.70.0\n\n`validateUserToken` from `@cotal-ai/auth` no longer takes `maxTtlSec`. It caps a bearer's lifetime\nat the cap of the bearer's view, the same cap the issuer applies when it mints: 900 seconds, or 300\nfor a `transfer-writer` bearer. The auth callout never passed the option, so a running mesh behaves\nas before. Only code of your own that calls the validator with `maxTtlSec` is affected.\n\n### What stops working\n\nA call that passes `maxTtlSec` in an object literal no longer compiles. Plain JavaScript that keeps\nit still runs, and the value is ignored. A `NaN` value, such as `Number()` of an unset environment\nvariable, used to turn the lifetime check off and accept a bearer of any lifetime. That bearer is\nnow refused at its view's cap.\n\n### Before the upgrade\n\nRemove `maxTtlSec` from each call. A test that needs a bearer to expire sooner mints one with a\nshorter lifetime.\n\n## Persisted identity records in 0.70.0\n\nThe manager instance identity, the manager sibling identities, the auth plane instance identity and\na participant manager's remote authority state now share one reader and one first mint in\n`@cotal-ai/workspace`, exported as `claimIdentityRecord` with the nkey check `identityOf`. Each\nrecord is read as a regular file, must hold non-empty nkeys and is created exclusively, so\nconcurrent first starts of a participant manager on one root now settle on one identity where each\nused to keep its own. `saveManagerInstanceIdentity` and `saveAuthInstanceIdentity` are gone. A\nrunning mesh whose records are plain files needs nothing.\n\n### What stops working\n\nA manager instance, auth instance or remote authority record that is a symlink, a directory or any\nother non-regular entry is refused where it used to be followed. The manager, the auth plane and a\nparticipant manager fail to start on it, and `cotal reconcile-gate` and `cotal deregister-instance`\nrefuse it. Retirement already refused it. A remote authority record with an empty nkey id or seed\nis refused too. A first mint that loses its race and cannot read the winner now refuses with\n`identity-record-create-lost` in place of `manager-instance-identity-create-lost` or\n`auth-instance-identity-create-lost`. Code that imports either `save` function no longer compiles.\n\n### Before the upgrade\n\nReplace a symlinked identity record with a copy of the file it points to. Code that wrote a record\nwith a `save` function plants it with `createManagerInstanceIdentity` or\n`createAuthInstanceIdentity`, which create the record when it is absent and otherwise return the\nstored one unchanged. Nothing replaces an overwrite of a stored identity.\n\n## Manager instance in user credentials in 0.70.0\n\n`AuthProvider.userCredentials` from `@cotal-ai/core` no longer returns `managerInstanceId`. A\n`manager-caller` credential's manager instance is the signed `act.managerInstanceId` claim in its\nbearer, which the broker verifies and the CLI already used. The reference provider in\n`@cotal-ai/auth` stops copying the exchange response's field into its result, where nothing\ncompared it with the bearer. The exchange still answers with the field, so a running mesh behaves as\nbefore.\n\n### What stops working\n\nCode of your own that reads `managerInstanceId` from a `userCredentials` result no longer compiles,\nand plain JavaScript reads `undefined` there.\n\n### Before the upgrade\n\nRead the instance from the bearer's `act.managerInstanceId` claim.\n\n## Auth plane identity location in 0.70.0\n\nThe user-auth service keeps its instance identity in the root's `.cotal/space.<hex>/auth-instance.json`,\nbeside the manager's. It used to sit inside `.cotal/auth`, at\n`space.<hex>/.cotal/auth/auth-instance.<hex>.json`, so a copy of that folder carried it. The first\nstart of an upgraded root moves the record and keeps the instance. A hosted context started through\n`startAuthService` has its record moved the same way inside its `stateDir`.\n\n### What stops working\n\nCode that calls `openAuthAuthorityPlane` without the new `identityRoot` option no longer compiles. A\nstart that finds a record both in `.cotal/space.<hex>/` and at its older place refuses and names the\ntwo files. A start also refuses when the older place of the auth or manager identity holds a symlink,\na directory or anything else that is not a regular file. The manager used to skip a dangling symlink\nthere and mint a new identity.\n\n### Before the upgrade\n\nPass `identityRoot` to `openAuthAuthorityPlane`. When `dir` is a workspace root's user-auth state\ndir, `<root>/.cotal/auth/space.<hex>`, pass that root. A plane with no workspace root, as\n`startAuthService` runs, passes `dir` itself. Either keeps the identity the plane already has: on the\nfirst start it moves from `<dir>/.cotal/auth/` to `<identityRoot>/.cotal/space.<hex>/`. Never pass a\ndirectory inside `.cotal/auth`: the record would land in the folder an operator copies and travel\nwith it again.\n\nA copy of `.cotal/auth` taken from a root last run by an older Cotal carries that root's record.\nDelete `.cotal/auth/space.<hex>/.cotal/auth/auth-instance.<hex>.json` from the root you copied it to\nbefore the first `cotal up --user-auth` there.\n\n## Per-seat `COTAL_` names in `spawn.env` in 0.71.0\n\n`spawn.env` in the cotal config no longer forwards a `COTAL_` name the launcher sets for each seat,\nsuch as `COTAL_ROLE`, `COTAL_MODEL` or `COTAL_SUBSCRIBE`. Before, a seat launched with no value of\nits own took the spawning process's value and ran under that role, model or read set. The\nmachine-wide knobs a seat already receives, such as `COTAL_HOME`, may still be listed.\n\n### What stops working\n\nEvery spawn and resume under a config whose `spawn.env` lists such a name is refused before\nlaunch, and the refusal names the entry. Code that calls `launchEnv` from `@cotal-ai/connector-core`\nwith such a name in `envAllow` gets the same error.\n\n### Before the upgrade\n\nRemove those names from `spawn.env`. Give each seat its role, model and channels with `--role`,\n`--model` and `--subscribe`, or in its persona's `role:`, `model:` and `subscribe:`.\n\n## Role addresses in 0.71.0\n\nA role must be one `[A-Za-z0-9_-]` token. Before 0.71.0 any other spelling was rewritten into one:\n` probe ` reached the `probe` queue and `pro.be` reached `pro_be`, while the message kept the\nspelling sent. An anycast to `*` was accepted and stored where no holder reads it.\n\n### What stops working\n\nAn agent whose role is outside the token set no longer starts, however it is launched:\n`cotal join --role`, `cotal spawn --role`, an agent file's `role:`, `COTAL_ROLE` and an embedded\nendpoint's `card.role` are all refused before the agent joins.\n\nA send to such a role, or to `*`, through `cotal send ask`, `/anycast` or `cotal_anycast` is refused,\nand nothing is stored.\n\n`routeToken` is no longer exported from `@cotal-ai/core`. A role routes as spelled, so code that\nused it to name a role's queue uses the role itself, and `assertValidRole` checks one.\n\n### What migrates on its own\n\nEvery task queue. A `svc_<role>` durable was always named from the rewritten token, so its pending\nrequests and its holders carry over.\n\n### Before the upgrade\n\nRename each role outside the token set to the token it already routed to: remove the surrounding\nspaces and replace every other character outside the set with `_`. Rename it where the holder is\nlaunched and in every script or prompt that sends to it.\n\n## IdP URLs on `localhost` in 0.73.0\n\nThe IdP URL that `cotal login --idp` and `cotal up --user-auth --idp` take, the JWKS URL the auth\nservice pins from it, the space-catalog link and `cotal sync` now share one rule: `https://`, or\n`http://` on a loopback IP literal. `localhost` is a name, so it no longer counts: a hosts entry\nwould choose the IdP, and with it the keys the callout trusts. Every loopback literal now passes, so\n`http://127.0.0.2/api/auth` and `http://[::ffff:127.0.0.1]/api/auth` are accepted where they were\nrefused before. A JWKS URL with any scheme other than `https:` or `http:` is refused. A mesh whose\nIdP uses HTTPS is unaffected.\n\n### What stops working\n\nOn a mesh whose IdP is pinned at `http://localhost:<port>/...`, the auth service builds its key\nresolver from the pinned JWKS URL at start, and that resolver now refuses it with\n`JWKS origin must be https (or http on a loopback IP literal for dev)`.\n\n`cotal login` and `cotal logout` with `--idp http://localhost:<port>/...` are refused with\n`idp url must be https (or http on a loopback IP literal such as 127.0.0.1 for local dev)`, and so is\nevery command that reads a session cached under that URL.\n\n### Before the upgrade\n\nUse `127.0.0.1` (or `::1`) in place of `localhost`. In the space's `idp.json` under the mesh's\n`.cotal/auth`, change `url` and `jwksUri` to the literal spelling and leave `issuer` and `audience`\nas they are. Owners derive from the issuer, so existing grants keep matching. Change a manifest's\n`broker.idp` the same way, because `cotal up` refuses an `--idp` that differs from the pin. Then\nhave each person run `cotal login --idp http://127.0.0.1:<port>/api/auth` again, because sessions\nare cached under the URL.\n\n## Terminal layout `Pane.confirm` in 0.73.0\n\n`Pane.confirm` is removed from `@cotal-ai/core`, and the tmux and cmux terminal-layout providers no\nlonger press Enter in the panes they open. The flag carried no prompt text, so both providers\npressed Enter five times, one second apart: the first dialog a pane showed was answered with its\ndefault, and a prompt that never appeared went unreported. Nothing in Cotal sets it, so a running\nmesh needs nothing. Spawned agents are unaffected, because their runtimes match the startup prompt\nby its text.\n\n### What stops working\n\nA `Pane` object literal that sets `confirm` fails to compile with TS2353. Plain JavaScript that sets\nit runs without an error, and the pane stays at its prompt.\n\n### Before the upgrade\n\nRemove `confirm` from each `Pane`. Start a command that shows a startup prompt with the option that\nskips it, or answer the prompt in the pane.\n\n## Carrying a resumed Claude session to another host in 0.67.0\n\n`cotal spawn --resume <id> --detach --on <instance>` now carries a Claude session held on the\noperator's host to the target manager instance. Both sides need this release: an older manager does\nnot serve `transcript-receive`, and the CLI then stops with that manager's refusal instead of\nlaunching. The manager cluster document moves to revision 21, and the `ps` row's `resume` object\ngains `host` and `transferredAt`.\n\nA manager host that runs carried seats needs `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN` or a\ncloud provider selection in its environment, because each carried seat runs in its own Claude home\nwith no stored login. On an authenticated mesh the CLI mints the transfer writer from the space's\nsigning seed, so the carrying host needs that seed, as for any other operator command. On a user-auth\nmesh it exchanges the operator's login for a `transfer-writer` view instead, so the operator's grant\nneeds scope `admin`, and the auth service must run this release. A remote manager receives a carry once\nits host serves the manager-service `transferReader` operation. A seat launched without carrying,\nincluding any `--resume` whose id this host does not hold, is unchanged.\n\n## Lifecycle head type in 0.67.0\n\n`LifecycleMapping`, the type `parseLifecycleHead` returns, is now a union on `state`. Nothing about\na running mesh changes: heads that parsed before parse the same way, and the refusals are\nunchanged. Only TypeScript code that compiles against `@cotal-ai/core` is affected.\n\n### What stops working\n\nAn `interface` that extends `LifecycleMapping` fails with TS2312, because an interface cannot extend\na union. Code that builds a head in memory no longer compiles when the head is `retiring` without\nits `op`, or `active` or `retired` with one. The parser already refused those heads.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type ActiveMapping = LifecycleMapping & { state: \"active\" }`. A reader that has checked\n`state === \"retiring\"` reads `op` without a guard.\n\n## Issuance gate types in 0.67.0\n\n`EpGateRow` and `EndpointGateRow`, which `parseIssuanceGate` and `parseEndpointGate` return, and\n`EpGateState`, which an `EpIssuanceGate` or `EpIssuanceBarrier` returns from `observe`, are now\nunions on `state`. Nothing about a running mesh changes: gates that parsed before parse the same\nway, and the refusals are unchanged. Only TypeScript code that compiles against `@cotal-ai/core`\nis affected.\n\n### What stops working\n\nAn `interface` that extends one of these types fails with TS2312, because an interface cannot\nextend a union. Code that builds a gate in memory, such as a custom barrier's `observe`, no longer\ncompiles when the gate is `frozen` or `retired` without its `op`, or `open` with one. The gate\nparsers already refused those rows.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type CustomGateRow = EpGateRow & { custom: string }`.\n\n## Lifecycle-blocked refusals in 0.66.0\n\nA refusal that carries `ai.cotal.ep.lifecycle-blocked` now reports only the lifecycle state it\nread. Nothing about a running mesh changes. A client that branches on the detail must read the new\nfield.\n\n### What stops working\n\nA refusal raised at the issuance gate used to carry `headState` without reading the head:\n`retiring` for a frozen gate and `retired` for a retired one. It now carries `gateState`\n(`frozen` or `retired`) and no `headState`. A client that treats `headState: \"retired\"` as a\nburned uid, or `headState: \"retiring\"` as a retirement in flight, no longer matches those\nrefusals, and the `[lifecycle ...]` suffix on the error string changes the same way. A custom\nissuance barrier whose `observe` returns a frozen gate without a valid `op` (a string `opId` and\none of the four op kinds) is now refused as `internal` by `registerServiceInstance`.\n\n### Before the upgrade\n\nUpdate such a client to read `gateState` for a gate refusal and `blockedOp` for the operation that\nholds the gate. `headState` is present only when the refusal read the head, for example an\nactivation refused because the head is still retiring.\n\n## Workflow programs that bind `once` in 0.65.0\n\n`once` is now a scope of the workflow language, so it is a reserved name. A program that declares\nits own `once` binding (`const once = ...`, a parameter or a function named `once`) is refused at\nvalidation with L2002. Nothing else about a running mesh changes.\n\n### What stops working\n\nA run whose recorded program binds `once` cannot be resumed after the upgrade, because a resume\nvalidates the recorded program again. A new `cotal run start` of such a program is refused before\nanything is recorded.\n\n### Before the upgrade\n\nList the runs with `cotal run ps` and check each program that is still running or held for a\nbinding named `once`. Let those runs finish on the old version before you upgrade the manager, and\nrename the binding in the program before you start it again.\n\n## From 0.58.0 to 0.59.0\n\nEvery connector now publishes a failed run's `RUN_ERROR` on `events.<owner>.<actor>` with the fixed\nmessage `run failed` and no `code` or `rawEvent`. The error text and error kind a harness reports\ncan echo a prompt, a peer message or tool output, and that channel has a different read ACL. A\nreader that showed the message or branched on `code` gets neither after the upgrade. Where a\nconnector reports the error kind as the agent's presence condition, that is unchanged.\n\n### Settle pending event frames before the upgrade\n\nEach session's events are frozen in its event write-ahead log before they are published. A session\nrestarted on 0.59.0 whose log still holds an unacknowledged frame with an older `RUN_ERROR` does not\nrepublish it: its event emitter halts with `egress-run-error` and publishes nothing further for that\nsession. The broker may or may not already hold that frame, so the halt cannot settle it.\n\n1. Stop the seats cleanly on 0.58.0, with the broker still up.\n2. List the logs that still hold a pending frame. The logs live under the events state root\n (`COTAL_WORKSPACE_ROOT`). Empty output means there is nothing to settle.\n\n ```sh\n find \"$COTAL_WORKSPACE_ROOT/.cotal/events\" -name wal.json \\\n -exec jq -r 'select(.pending != null) | input_filename' {} +\n ```\n\n3. For each session listed, start it again on 0.58.0 while the broker is reachable, let it recover,\n stop it, and run step 2 again. Recovery publishes the frame as 0.58.0 would have, error text\n included, so it only finishes what 0.58.0 had already started.\n\n If that start halts with `cas-loss` instead, the agent's subject is no longer at the sequence this\n log expects, and no restart settles that log, on 0.58.0 or later. A lost acknowledgement is one\n cause: the broker stored the frame, so it and its error text are already on the channel, and every\n retry halts the same way because the stream checks the frozen expectation before it deduplicates.\n The halt message names the other causes, such as a second emitter for the same agent under a\n different state root, a restored stream or frontier record, or a purged channel. With those the\n pending frame may never have reached the broker, so a `cas-loss` does not tell you whether it\n landed. Find and stop any second writer and rule out a restored state first. Clearing the halt\n then means purging the agent's event channel and removing the agent's directory under the events\n state root whole (see [Event plane](connect-claude.md#event-plane)). That abandons the pending\n frame whether or not the broker has it, and the purge also drops the earlier frames of every\n session of that agent.\n4. Upgrade once step 2 prints nothing.\n\nIf a session halts with `egress-run-error` after the upgrade, go back to step 3 for that session on\n0.58.0. Do not edit or delete `wal.json` on its own to get past either halt: clearing the pending\nframe abandons that epoch, an event the broker never received is lost, and removing part of the\ndirectory leaves a state the next start refuses.\n\n## Explicit actor grants in 0.59.0\n\n`cotal actor grant` no longer fills an omitted ACL flag with its wide default. A grant names\n`--scope`, `--allow-subscribe` and `--allow-publish`, or passes `--full` to give the ones it leaves\noff their wide defaults (`spawn,role:default`, `>` read, `>` post). Any other grant is refused. The\nbreak is in the CLI on the machine that holds the actor ledger, the one that ran\n`cotal up --user-auth --idp <url>`. No stored row, credential or wire message changes.\n\n### What keeps working\n\nExisting actor ledger rows keep the authority they were granted, and their users and agents connect\nas before. `actor revoke`, `actor list` and a `grant` that names all three ACL flags behave as they\ndid on 0.58.0. Nothing on disk is converted.\n\n### What stops working\n\nA grant that leaves off any of the three flags without `--full` exits 1 with\n`refusing to grant \"<actor>\" with --scope, --allow-subscribe, --allow-publish left off`, naming the\nflags it is missing, and then prints both accepted forms. It writes no row and does not retire the\nactor's current lifecycle. An existing row stays as it was, and an actor granted for the first time\nstays out until the grant is run again. This includes the bare grant printed on 0.58.0 by\n`cotal login`, `cotal status`, `actor list` and the not-granted refusal. Look for it in provisioning\nscripts, onboarding runbooks and anything that pastes those hints.\n\n### Upgrade order\n\nChange the scripts before the ledger machine is upgraded, and make each grant name all three flags.\n0.58.0 and 0.59.0 both accept that form. To keep a wide row, write its defaults out:\n\n```sh\ncotal actor grant <actor> --sub <IdP subject> \\\n --scope spawn,role:default --allow-subscribe '>' --allow-publish '>'\n```\n\nSwitch to `--full` only once the ledger machine runs 0.59.0. 0.58.0 refuses it with\n`Unknown option '--full'` before it reads the ledger. Brokers, managers and participant machines\nneed nothing for this break, so their order is the one the section above gives.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused grant changes nothing. The\nexposure is a grant script that runs against 0.59.0 before it was changed: it fails and grants\nnothing.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. On the ledger machine, save the output\nof `cotal actor list` to compare rows after the changed scripts run, and list the scripts that call\n`cotal actor grant`.\n\n### The upgrade end to end\n\n```sh\n# on the ledger machine, still on 0.58.0\ncotal actor list > actors-before.txt\ngrep -rn 'actor grant' <your provisioning scripts>\n# make every grant name --scope, --allow-subscribe and --allow-publish, run them, then upgrade\nnpm i -g cotal-ai@0.59.0\ncotal actor list | diff actors-before.txt -\n```\n\nBoth refusals quoted here were run on 0.58.0 and on the 0.59.0 code. That brokers, managers and stored\nrows need nothing is read from the change, which touches only the CLI and its hints, and was not run\non a live split deployment.\n\n## Repeated flags refused in 0.59.0\n\nA `cotal` flag given more than once is now a usage error unless the command declares it repeatable.\nOn 0.58.0 the last value won with no message, so `cotal down web --space a --space b` acted on `b`\nwhile a wrapper that checked the first `--space` verified `a`. The break is in the command-line\nparser on the machine that runs the command, including commands added with `cotal ext add`. No\nstored state, credential or wire message changes.\n\n### What keeps working\n\nA command line that gives each flag once parses as it did on 0.58.0, in any order and in the\n`--flag=value` form. Flags whose help says repeatable, such as `--opt` and `down --session-store`,\nstill collect every value. A flag-shaped word after `--` is still a positional. The daemons, units\nand agents that `cotal` starts for itself are given each flag once, so a fleet driven only by `cotal`\ncommands typed by hand needs no action.\n\n### What stops working\n\nA command line that repeats any other flag exits 1 before the command runs. It prints\n`Option '--space' cannot be repeated`, or `Option '-f, --file' cannot be repeated` for a flag with a\nshort form, followed by the command's help. `-f` and `--file` count as the same flag. Look for it in\nscripts, aliases and wrappers that append a flag to override one set earlier, such as a fixed\n`--space` followed by `\"$@\"`.\n\n### Upgrade order\n\nChange those scripts first so each flag is given once. 0.58.0 and 0.59.0 both accept that form.\nBrokers, managers and participant machines need nothing for this break, and each machine's CLI\napplies it when that machine is upgraded, so their order is the one the sections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused command does nothing. The\nexposure is a script that still repeats a flag when it runs on 0.59.0: it exits 1 instead of acting on\nthe last value.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the scripts, aliases and wrappers\nthat call `cotal` so each one can be checked.\n\n### The upgrade end to end\n\n```sh\n# still on 0.58.0\ngrep -rn 'cotal ' <your scripts and wrappers>\n# give each non-repeatable flag once, then upgrade\nnpm i -g cotal-ai@0.59.0\n# run each changed script; a repeat left behind exits 1 with the usage error and does nothing\n```\n\nThe refusal and its messages were run against the 0.59.0 parser and `cotal topology view`. That the\nargument lists `cotal` builds for its own processes give each flag once is read from the code, and\nwas not run on a live split deployment.\n\n## Detached spawns from a seat's shell in 0.62.0\n\nOn a static or open mesh, `cotal spawn --detach` run inside a managed seat's shell now launches as\nthat seat. On 0.61.0 it minted a one-shot operator instrument, so the manager recorded that\ninstrument as the spawner and the seat's own `cotal_despawn` of the child was refused with\n`not authorized: <seat> was not spawned by <caller> (admin tier required)`. The break is in the CLI\non the machine where the seats run. No stored state, credential or wire message changes.\n\n### What keeps working\n\n`cotal spawn --detach` from an operator terminal or from a script outside any seat launches as\nbefore, and so does any call with `--creds`, one aimed at a space other than the seat's own, or a raw\nopen target named with `--server` and an unregistered `--space`. A user-auth mesh is unchanged. A seat with\n`capabilities: [spawn]` still spawns from its shell, and can now stop that child with\n`cotal_despawn`. `--on <instance>` from a seat's shell still lands on that manager instance, now as\nthe seat.\n\n### What stops working\n\n- On a static mesh, a seat without `capabilities: [spawn]` can no longer spawn from its shell. Its\n own credential holds no spawn subject, so the broker refuses the request and the command exits 1.\n- A child launched from a seat's shell is now that seat's child, so the manager stops it when the\n seat exits, as it does for a `cotal_spawn` child. A child that has to outlive the seat that\n started it now goes with the seat.\n- A seat launched without `COTAL_SPACE` is placed by its static credential. Every connector sets\n that variable, so this only reaches a hand-built launch: from such a seat's shell, a spawn aimed at\n a static space that holds no credential for the seat is refused instead of running as the operator.\n\n### Upgrade order\n\nOnly the CLI that seats run from their shell changes, which is the one installed on the host where\nthe seats run. Brokers and managers need nothing for this break, so their order is the one the\nsections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it. A child already running when you upgrade\nkeeps the spawner the manager recorded at its launch.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the agent files whose seats run\n`cotal spawn --detach` from their shell, note which of them lack `capabilities: [spawn]`, and note\nwhich of their children must outlive the seat.\n\n### The upgrade end to end\n\n```sh\n# still on 0.61.0: find the seats that spawn from their shell\ngrep -rln 'cotal spawn' .cotal/agents\n# add `capabilities: [spawn]` to each of those agent files that lacks it, and launch any child\n# that must outlive its seat from an operator terminal instead\nnpm i -g cotal-ai@0.62.0\n```\n\nThe attribution, the despawn, the refusal of a seat without `spawn`, the stop on seat exit and a\nseat's `--on` spawn were run on a local static mesh, and the attribution and the despawn on a local\nopen mesh.\n\n## From 0.53.0 to 0.54.0\n\nManager calls now borrow an instance-bound `manager-caller` credential. Followed mutations require\n`manager.goal-result` on the selected manager, so a compatible issuer, manager and client must be\nloaded together. An older manager is refused before a followed mutation; upgrading an installed\nbinary alone does not replace code in a running manager, connector or embedded client.\n\n### Preserve state before changing processes\n\nSnapshot the broker's durable storage using its supported backup procedure, the host authority and\nactor ledgers, and each participant's manager identity, runtime custody records, credentials and\nsaved sessions. Include the embedding application's database and configuration under its supported\nbackup procedure. Record the loaded package versions and the CLI path used by bearer helpers.\nKeep these copies private. Do not change the IdP issuer, regenerate manager identities, rotate agent\ncredentials or recreate tenant storage to make the upgrade pass.\n\nNo ledger, goal-history or session conversion is required for this change. Existing ordinary\nmessaging credentials retain their normal expiry rules. New manager-caller credentials are obtained\non demand from the current grant; old manager-call credentials do not gain the new view automatically.\nExisting accepted goals remain durable and must not be submitted again merely because observation\nwas interrupted. Fresh remote registration publishes its service status at the current revision and\nepoch; do not seed that status manually.\n\n### Upgrade the split deployment\n\n1. Stage one pinned 0.54.0 package set for the host and participants, including the embedding SDKs.\n Pause new manager mutations and let accepted work settle where possible before reloading processes.\n2. Upgrade the host issuer and embedding first. Keep the broker, its account identities and durable\n storage in place. Then load the matching manager release on each participating machine.\n3. Preserve active seats through the runtime's supported update path. A Linux custodial runtime may\n release and re-adopt seats within its 600-second unattended window; verify the actual runtime,\n custody records and process identities before relying on it. A legacy PTY runtime without release\n support cannot preserve active seats through a generic manager restart. Drain it at an approved\n idle window instead of signalling the manager or replacing conversations.\n4. Reload the clients and connectors through their session-preserving host controls. Refresh any\n bearer helper captured from an older immutable CLI path. A transport-only reconnect does not reload\n JavaScript. Verify authenticated instance selection, a read-only manager command and canonical\n result recovery before allowing new followed mutations.\n\nTreat the interval from issuer reload through compatible manager/client reload as a manager-control\noutage. Mixed versions can refuse discovery or commands; there is no promised rolling transition.\nOrdinary agent sessions survive only where their runtime and credentials permit it. If verification\nfails, keep mutations paused and repair forward from the preserved state rather than resetting it.\nThis release does not add host-backed enrollment or terminal release for stock participant detached\nagents; see [Remote supervised agents](run-a-mesh.md#remote-supervised-agents).\n\n## From 0.48.2 to 0.49.0\n\n0.49.0 changes how a credential's authority is recorded. A credential is no longer only a signed\nfile: it is an *issuance*, with a generation the issuer chose and durable evidence of the ceiling it\nwas granted under. The important consequence for a running deployment is not at connect time. It is\nat renewal time.\n\n### What keeps working without any action\n\n- **Existing agent credentials keep authenticating.** A credential minted under 0.48.2 is not\n revoked and is not rejected at connect. Nothing needs to be re-issued to bring the fleet back up\n after the upgrade.\n- **The channel registry survives.** Channels, their replay settings, descriptions, and usage text\n are ordinary durable state and are not rewritten by the upgrade.\n- **`cotal deliver` is still a standalone command.** Running the delivery daemon as its own process\n remains supported; it is not restricted to being a child of `cotal up`.\n- **`cotal join` keeps its flags.** In particular `--lifecycle-uid` is not new in 0.49.0. It has\n been required alongside `--creds` since well before this release, and the pairing rule did not\n change here. A scripted external join that worked under 0.48.2 works unchanged.\n\n### What does not migrate\n\n**A credential minted before 0.49.0 cannot be renewed.** Managed agent credentials carry a\n24-hour lifetime and the manager re-signs one once it passes **75%** of its life, ticking every\nquarter of the TTL so a tick always lands inside that window. When the manager reaches a credential\nthat carries no issuance, it refuses to renew it and logs the agent by name:\n\n```\n! managed cred renewal <agent>: renewManagedStaticCred: <agent> carries no issuance;\n a static credential minted before SPEC 13.15 is not renewed under an unbound generation\n - respawn the agent\n - the agent dies loud at this cred's expiry unless it is reminted\n```\n\nSo the fleet comes up fine, runs normally, and then each agent stops at its own credential's\nexpiry, within roughly a day of the upgrade, one at a time rather than together. The refusal is\ndeliberate: the renewal would otherwise have to invent a generation nobody issued, which is the\nstate the release exists to remove.\n\n**Respawn the managed agents as the last step of the upgrade.** For this particular upgrade the\nrespawn is not optional: stopping a 0.48.2 manager ends its agent processes whichever CLI you use,\nfor the reason given under the outage window below. The respawn is how they come back, and it is\nalso what mints each credential as an issuance so it renews from then on. One planned pass over the\nfleet is the whole job. Skipping it leaves agents stopped and, for any credential that survived\ninto 0.49.0 unminted, brings the renewal cliff above a day later, one agent at a time.\n\n### Credentials you minted yourself\n\n**A credential you minted with `cotal mint` is a different case, and it very likely needs\nnothing.** The distinction that matters is not the word \"static\", which covers both. It is **what\nminted the credential and who owns its renewal**. A credential the **manager** minted for an agent\nit spawned carries a lifetime and is renewed by the manager, so it is the subject of everything\nabove. A credential **you** minted with `cotal mint` and handed to an external peer is issued with\n**no expiry at all**, and no manager renews it: it is not in the sweep, so there is no renewal to\nfail. It keeps working after the upgrade, and re-minting it would mean coordinating with a third\nparty for no gain.\n\nThe manager says which one it is holding. Where a credential has no expiry to reach, the sweep\nnames it and moves on rather than refusing:\n\n```\n! managed cred renewal <agent>: credential is unbounded - not renewed\n (a pre-TTL credential stays as minted until respawn)\n```\n\nRe-mint an external peer's credential only if you want it to carry a lifetime, and at a time you\nchoose.\n\n### How to read the boot log\n\nA 0.49.0 manager starting over an existing space may print lines like:\n\n```\n verified evicted: <holder-key> (3/12)\n already verified (durable): <holder-key>\n\u2713 boot self-heal: manager/<id> registration gate reopened at generation <n>\n```\n\nThese are **not** a credential migration, and reading them as one is the most likely way to\nconclude the fleet is fine when it is not. They come from the manager repairing **one** endpoint\nregistration gate that a previous restart left frozen, and they enumerate that single gate's\ncredential-family holders as it verifies each one evicted. `already verified (durable)` on a later\nstart is the repair cursor resuming, not a credential that became durable. The repair is real and\nuseful (it is what previously needed `cotal reconcile-gate` by hand), but it says nothing about\nwhether your agent credentials carry issuances. The renewal refusal above is the signal that does.\n\n### Which side to upgrade first in a split topology\n\nMove the manager first.\n\nThe stores 0.49.0 introduces are created by the **manager** at its own boot, not by the broker.\nThey are create-or-verify and idempotent, so a 0.49.0 manager brings the space's authority stores\nup to the new shape itself, and it does so against whichever broker is answering.\n\nBeing honest about the evidence behind each direction, because they are not equally established:\n\n- **Broker-first was measured on a live 30-agent deployment** (issue #1578). Upgrading the broker\n first locks the old manager out immediately: `cotal up` re-renders the broker's generated config\n from the trust record, and after the restart the still-0.48.2 manager is refused on every\n connection with an `authentication error` naming the Nkey, continuously. That text comes from the\n broker process, not from a Cotal command, so match on its shape rather than on an exact string.\n `cotal ps` reports zero agents while\n the agent processes are still alive, because the manager has lost its view of them, not because\n they died. Upgrading the manager clears it immediately.\n- **Manager-first is reasoned from where the new stores are provisioned**, not from a measured\n fleet upgrade. It is the recommended order because the manager is the component that creates what\n 0.49.0 adds, but it has not been run end to end on a production split topology at the time of\n writing. Treat it as the better-supported order rather than a guaranteed one, and keep the\n rollback below ready either way.\n\nWhichever order you pick, **this is not a rolling upgrade**. Between the two steps the mesh is down\nand the manager cannot see its agents. Go straight through rather than pausing between them, and\nschedule it as an outage window.\n\n### What the window looks like\n\n- **The managed agent processes do not survive step 1, in either order.** This is the one place\n where the obvious reordering does not rescue you, so it is worth understanding rather than\n working around. Sparing agents on a bare manager stop is a **handshake**: a 0.49.0 manager\n publishes a capability file proving it can release its agents, and a 0.49.0 `cotal down` refuses\n the stop unless it finds one. **A 0.48.2 manager never publishes that file**, because the\n mechanism ships in the release you are installing. So the old CLI against the old manager sends a\n plain stop and takes every seat with it, and the new CLI against the old manager either refuses\n (leaving `--with-agents`, which reaps deliberately) or falls to the legacy path, warns that it\n cannot verify the manager can spare its agents, and signals it anyway.\n- **You can confirm which side you are on in one command, without stopping anything.** The flag that\n marks the newer behaviour is absent from the older CLI, and its summary line makes the difference\n plain:\n\n ```\n $ cotal down --help # on 0.48.2\n cotal down - stop the whole local stack, or name only the components to stop\n\n $ cotal down --help # on 0.49.0\n cotal down - stop the whole local stack (managed agents stay running unless --with-agents), ...\n ```\n\n If your `cotal down --help` does not mention `--with-agents`, stopping the manager stops the\n agents with it.\n- **Therefore the respawn in step 5 is mandatory recovery for this upgrade, not an optional pass.**\n It is also the step that re-mints credentials as issuances, so it is the same action either way.\n Plan the window to include it rather than treating it as cleanup.\n- The **manager's view** of them is lost while the two sides disagree, so `cotal ps` reports zero\n and control commands do not reach seats.\n- **Messages are not delivered** while the mesh is down.\n- The window is as long as it takes to restart the second component, plus the manager's own start.\n It is minutes, not hours, provided you do not stop between the steps.\n- **Nothing self-heals if you stop halfway.** The refusal is continuous until both sides match.\n\n### Snapshot this before you start\n\nTake these while the deployment is still on 0.48.2. The two `cotal` reads are live reads and must\nhappen before anything stops.\n\n- **A filesystem or volume snapshot of both containers**, if your platform offers one. This is the\n only rollback that covers every case, and it is what the reporting deployment used.\n- **`cotal backup create <dir>`**, for the durable space state, **but read the next paragraph before\n you rely on it**: on a split broker and manager topology it is very likely unavailable to you, and\n the volume snapshot above is your actual rollback.\n- **The trust records and credential directory** under `.cotal/auth` on the manager host, including\n the per-space material directory. These are what a re-mint would otherwise have to replace.\n- **A copy of the channel registry**, so you can verify it came back rather than assuming it did:\n `cotal channels list` before and after.\n- **The output of `cotal ps`**, so you know how many seats you expect to see afterwards and can tell\n a lost view from a lost agent.\n\n#### `cotal backup` on a split topology\n\n**`cotal backup create` cannot read a running stack.** It requires a completed cut, and only\n`cotal down --preserve-state` publishes one:\n\n```\n$ cotal backup create ./backup.0482\n\u2717 backup requires a completed cut; run `cotal down --preserve-state` first\n```\n\n**And `cotal down --preserve-state` requires a manager alive on the host you run it from.** It uses\nthat manager to attest that every retained child stopped, and the check is deliberately fail-closed:\na manager that is dead or merely uncertain refuses rather than preserving an unproven cut. The check\nreads a local pidfile, so a **remote** manager does not satisfy it. On a split topology the broker\nhost has no local manager, which means the documented durable-backup path is not available there.\n\n**Measured rather than assumed, at 0.48.2**: the backup refusal above is executed output. The\npreservation requirement is read from `down.ts` at the same tag, where the preserve path asks a\nmanager to prepare an inventory and then requires that manager to be locally alive before it\ncommits. The part not executed end to end is a genuine two-host split, which needs two real hosts.\n\n**What to do instead.** Use the filesystem or volume snapshot of both containers. That is the\nrollback the reporting deployment actually used, it covers the broker's durable state and the\nmanager's credential material together, and it does not depend on either component being able to\nattest for the other. If you want `cotal backup` as well, take it from a host that does have a live\nlocal manager, and understand it is a second copy rather than the primary rollback.\n\n**This looks like a product limitation rather than a documentation gap**, and it is written here as\none so an operator is not left thinking they mis-typed a command. The upgrade path for the exact\ntopology this page is addressed to cannot use the documented backup command.\n\n### The upgrade end to end\n\n```bash\n# 0. on 0.48.2, STILL RUNNING: record what you expect to see afterwards.\n# These two are live reads, so they must happen before anything stops.\ncotal channels list > channels.before\ncotal ps > ps.before\n\n# 1. manager host. READ THE NOTE BELOW THE BLOCK FIRST: this step ends the\n# managed agent processes whichever order you choose, and the respawn in\n# step 5 is how they come back. It is recovery, not tidying.\n#\n# STOP THE MANAGER WITH THE 0.48.2 CLI, BEFORE INSTALLING 0.49.0. The\n# order matters and it is not recoverable once you install: a 0.49.0\n# `down manager` REFUSES to stop a 0.48.2 manager whose pid record carries\n# a start token, which is every manager on a platform that can read one\n# (Linux can):\n# refusing bare manager stop: ... does not prove this manager can detach\n# its agents; use --with-agents or stop the agents explicitly\n# The refusal names two remedies and NEITHER clears it for this case. The\n# check reads a capability file that only a 0.49.0 manager writes; it never\n# counts agents, so stopping them first changes nothing. And `--with-agents`\n# is whole-stack only, so `down manager --with-agents` is refused by its own\n# flag rule. See #1592.\ncotal down manager # the 0.48.2 CLI, still installed.\n # 0.48.2 has no --with-agents; this\n # is the whole route. On a host that\n # runs the whole stack, the 0.49.0\n # `cotal down --with-agents` after\n # installing is the alternative.\nnpm install -g cotal-ai@0.49.0 # ONLY after the stop above\n# `supervise` RUNS IN THE FOREGROUND and holds the terminal until you stop\n# it. There is no --detach on this command. Start it under whatever keeps\n# your manager alive normally (systemd unit, container entrypoint, or a\n# second terminal), and run the remaining steps from another shell.\ncotal supervise --space <space> --server nats://<broker>:4222\n\n# 2. broker host: stop the stack.\n# NOT `--preserve-state` on a split topology: it needs a manager alive on\n# THIS host to attest its children stopped, and yours is on the other one.\n# Your rollback is the volume snapshot from \"Snapshot this before you\n# start\", not `cotal backup`.\n# See \"cotal backup on a split topology\" above.\ncotal down\n\n# 3. broker host: install 0.49.0 and start it again\nnpm install -g cotal-ai@0.49.0\n# Record the manager log's size BEFORE starting, so step 3a can tell THIS\n# boot's output from every earlier one. It must be captured here, ahead of\n# the start: taken afterwards it sits past the new line and the wait hangs.\n# `<spaceKey>` is NOT the space name. It is lowercase hex of the name's\n# UTF-8 bytes, so space `prod` is `manager.70726f64.log`. Do not guess it:\n# `cotal up` prints the real path on its launch line. Substituting the\n# plain name points at a file that does not exist, and the wait below then\n# burns its full timeout before telling you.\nLOG=.cotal/manager.<spaceKey>.log\nOFF=$( [ -f \"$LOG\" ] && wc -c < \"$LOG\" || echo 0 )\ncotal up --detach --host 0.0.0.0 --space <space> --no-manager\n\n# 3a. SPLIT TOPOLOGY ONLY: `--no-manager` above boots the broker (and the\n# delivery daemon) with NO local manager on the broker host, so there is\n# no wait-and-stop step on a current cotal-ai. The rest of this step is\n# the OLDER-host recipe, kept because the flag is refused there and that\n# refusal is your signal you are on it: without the flag the `up` also\n# starts a local manager, and you must wait for the log to show it is up,\n# then stop it, or you finish the upgrade with two managers and the one\n# you did not intend is the one nobody is watching.\n# A bare `grep -q` does NOT wait: it reads once and exits 1 immediately\n# if the line has not been written yet. Bound the wait instead, so a\n# manager that never comes up fails loudly rather than reading as ready.\n# The log is opened APPEND-ONLY, so on any host that has run a manager\n# before, this file ALREADY carries a `manager up` line from an earlier\n# boot. Grepping the whole file therefore matches instantly and waits for\n# nothing. Read only what THIS boot appended, using the $OFF captured in\n# step 3 above (before the start, which is the only point it is correct):\ntimeout 60 bash -c \\\n \"until tail -c +$((OFF+1)) \\\"$LOG\\\" | grep -q '. manager up'; do sleep 1; done\"\n# exit 0 = THIS boot logged it; exit 124 = it never did, so STOP and look.\n# This manager is 0.49.0 and publishes its own spare-capability file, so\n# the bare stop below is NOT the refusal case from step 1.\ncotal down manager # broker + delivery remain\n# On a current cotal-ai the two commands above are unnecessary (nothing\n# to wait for, nothing to stop) and `cotal down manager` simply reports\n# no manager to stop.\n\n# 4. verify the mesh is whole again before touching the fleet.\n# Do NOT compare `cotal ps` against ps.before yet: step 1 ended the agent\n# processes, so at this point it is EXPECTED to be empty, and an empty\n# `ps` is also the signature of the broker/manager mismatch described\n# above. The two are indistinguishable here, so compare what the mesh\n# itself should have carried across instead:\ncotal channels list # compare against channels.before: this SHOULD match now\ncotal ps # expect it to be EMPTY here; ps.before is the target for\n # step 5, not for this step\n\n# 5. the step that is easy to skip: respawn the managed agents so their\n# credentials are re-minted as issuances and can renew. Persona is a\n# POSITIONAL argument here, unlike `cotal stop`, which requires --name.\n# One call per agent:\ncotal spawn <persona> --detach --name <n> --space <space>\n# then the comparison step 4 could not make:\ncotal ps # NOW compare against ps.before: seat count should match\n```\n\nThe mesh is down from step 2 until step 3 finishes. That is the window. On a split topology there is\nno cut and no backup inside it, so the window is the stop, the install and the restart, nothing more.\n\n## Adding a section for a future release\n\n**Every changeset marked breaking adds a section to this page.** A release that changes what an\noperator must do, in what order, or what stops working, is not finished until the section exists.\n`scripts/upgrade-section-gate.mjs` grades a commit range for this: run it as\n`pnpm upgrade-section-gate --base <ref>` and it reds when the range carries a breaking change and\nadds no new release section. CI runs its self-test and, as a step of the `attribution` job, grades\neach pull request's own range as `HEAD^1..HEAD` over the merge snapshot it checked out. That job is\nthe only context in the branch protection rule set, so a red gate FAILS A REQUIRED CHECK AND BLOCKS\nTHE MERGE. The section is not optional and a reviewer cannot wave it through without an\nadministrator overriding branch protection. Be precise about what the check proves either\nway, because one trusted past its evidence is worse than none. It proves a section for a release\n**was written here**. It cannot prove the section is **correct**, or that it describes the break\nthat actually landed, and it cannot see a breaking change that carries no marker at all. Reviewing\nthe words remains a person's job.\n\n**Mark the break, or the gate cannot see it.** Any one of these is enough, and they are the only\nthings it reads:\n\n- a `!` before the colon in the commit subject, as in `feat(core)!: bind hosted runs to the caller`\n- a `BREAKING CHANGE:` footer in the commit body\n- a changeset in `.changeset/` declaring a `major` bump for any package\n\nThe marker must survive the squash. A `!` that lives only in a commit you squash away is not in the\nrange the gate grades, so put it in the subject that lands on `main`.\n\n**The heading is a `##` and names the release**, like `## From 0.48.2 to 0.49.0`. Both matter, and\nneither is a style preference. Coverage is claimed by a heading, so a heading that names\nno release claims every release and distinguishes none: `## Notes` with a sentence under it would\notherwise satisfy the rule. Naming the release also makes the section the one an operator upgrading\nthat release will search for. Use `###` freely for detail inside a section. Subsections belong to\ntheir release rather than counting as separate coverage.\n\nName the release that first carries the change: the next version Changesets publishes, which\n`pnpm changeset status --verbose` lists. `bin/package.json` on `main` still reads the release already\npublished. If a release is cut while the change is open, the change ships in the release after it,\nso move the heading before merging. The gate accepts any version in a heading, so before merging a\nrelease pull request, check every heading added since the previous tag against the version it\npublishes.\n\nA section is written for the operator, not for the reviewer. It answers, in this order:\n\n1. What keeps working with no action at all.\n2. What does **not** migrate, and when that becomes visible. Name the log line if there is one.\n3. The order to move components in for a split topology, and why that order.\n4. What the outage window looks like, including what survives it.\n5. What to snapshot before starting.\n6. The commands, end to end.\n\n**Where an answer was not measured, say so in the document rather than guessing.** An operator who\nknows which half of a recommendation is reasoned and which is measured can plan around it; one who\nfinds out afterwards cannot.\n\n## Handle redemption in 0.91.0\n\n`auth redeem <private-handle-file>` selects the native auth target, then refuses unsupported\nconferring-handle redemption without reading the file or issuing credentials. It does not redirect\nthe artifact into agent enrollment or interactive-session redemption. Offline handle inspection\nstill labels signatures, revocation and holder currency not checked. An explicit `--space`\nrequests static-registry signature inspection with a native ephemeral operator reader. It checks\nsignatures, issuer scope, presented links and required revocation records without establishing\npresenter identity, live epochs or use authority. It confers no holder credential, persists no\nreader credential, and never turns missing or stale native records into verified offline success.\nOpen/user-auth inspection and conferring redemption remain unsupported.\n\n## Dashboard service transition in 0.91.0\n\nThe matching dashboard package now registers a service preset and hidden native child, not a\npublic web command. An older cached command descriptor is hidden and refuses with the matching\nextension-update remedy before importing provider code or repairing the install. `update` remains\nexplicit; `service start web` never installs a deliberately absent extension.\n\nConfigure the saved web definition with its existing registered static-auth root, one protected\ncredential file and explicit read-only intent. The service retains required TLS, creates no caller\nor channel-purger credential, opens no browser and prints no bootstrap secret into logs. The page\nmarks it read-only and hides delete; the server independently rejects that mutation before any\nalternate connection. The private session file holds the full-launch bootstrap; logs give its\nreference only. User-auth/open targets and OS installation remain unsupported in this route.\nExisting legacy process records are still observed, never silently adopted. An already-running\nlegacy dashboard is not restarted or converted by loading metadata. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\n## Registered lifecycle retirement in 0.91.0\n\n`auth retire <full-lifecycle-uid> [--yes]` has closed UID/space input, but the standalone CLI still\nlacks a supported native retirement instrument carrying its own current serve identity and gate.\nIt refuses without publishing a request. It does not borrow the manager's identity, treat --yes as\na grant, or call the host's interactive/managed loopback doors as a substitute. This remains an\nexplicit authority gate, not a completed retirement operation.\n\n## Local-process `rootedAt` removal in 0.91.0\n\nA local-process extension descriptor no longer has a `rootedAt` field. No stop has read it since\nthe dashboard became a service. Operators have nothing to migrate, snapshot or order, and there is\nno outage: a manifest that an older install cached with the field still loads, and the next\nmanifest rewrite drops it. An extension author removes the field from each `LocalProcess`\ndeclaration, where TypeScript now reports it as an unknown property.\n"
|
|
27287
|
+
"body": "# Upgrading a running deployment\n\n> **Guide** (informative) \xB7 **For:** operators upgrading a mesh that already exists \xB7 **See also:** [Substrate stability](stability.md), [Run a mesh](run-a-mesh.md), [Identity and auth](identity-and-auth.md)\n\n[Substrate stability](stability.md) tells you what the version numbers promise. This page is the\nother half: what to actually do when the deployment already exists, has credentials in it, and\ncannot simply be recreated. Every release that breaks a running deployment gets a section here,\nnaming what migrates on its own, what does not, and the order to move the pieces in.\n\n## The pre-1.0 upgrade contract\n\nThe packages are pre-1.0, so a minor bump may break an API or an on-disk expectation. Four\ncommitments make that survivable for someone with a fleet:\n\n- **Pin an exact version.** `0.N.P`, never `^0.N.P`. A range can pull a breaking minor in during an\n unrelated reinstall.\n- **Every break that touches a running deployment gets a section on this page**, written in terms of\n what an operator does, not in terms of which module changed.\n- **Read the section before you start, not halfway through.** A section names the work up front\n precisely so the operation does not change shape once it is underway.\n- **A break that cannot be made automatic says so.** Where credentials or state must be recreated by\n hand, the section says which ones and when, rather than leaving you to discover it at the moment\n the first one stops working.\n- **A change to the shape of a credential, or to who may renew one, is breaking whatever the commit\n marker says.** This rule is stated because the marker is a judgement made while writing the code\n and the consequence is felt by someone running it a day later. A fleet that keeps authenticating\n looks compatible and is not, if nothing in it can renew. Any automated check of this rule would\n read commit markers, so a break recorded as a feature is the one case it could not see, which is\n why the rule is written for people first. **The marker held for this release: the 0.49.0 change\n that caused all of this, `36d177951 feat(core)!`, did carry its `!`.** The rule exists for the\n next one that does not.\n\nWhat this page does not promise is a rolling upgrade. Nothing in the current line dual-serves two\nauthority versions, so where broker and manager run separately there is a window in which the mesh\nis down. The sections below give that window's shape so it can be scheduled rather than endured.\n\n## Cooperative workflow cancellation in 0.92.0\n\nThe manager adds the issued, instance-bound `run-cancel` command. Its cluster revision advances to\n37, retaining the vitals and keyed-submission commands. Cancellation is available through the\ngeneric endpoint client; this change adds no CLI or MCP cancellation shortcut.\n\n### What keeps working\n\nExisting run records, admissions, revocation markers and journals keep their formats. Upgrade does\nnot rewrite them, cancel a run or remove a marker. A released run without a marker can still resume\nunder its admission. A revoked run remains revoked. Manager shutdown still requests boundary\nrelease; it does not write a cancellation marker or prove an agent process stopped.\n\n### What needs an update\n\n- Custom `RunHost` implementations must return a drive with `cancel(reason)`, in addition to\n `done` and `release(reason)`. It must propagate cooperative root cancellation to in-flight\n effects. Do not substitute boundary release or process termination. Rebuild and qualify the\n custom host against the new core contract before enabling it on an upgraded manager.\n- The built-in runtime and language packages carry the root signal through both engines and the\n compiled worker. Keep core, auth, manager, runtime and language on the same release; copying one\n source file or replacing only the manager does not supply the new protocol and worker code.\n- Existing caller credentials do not gain a new publish permission on installation. Obtain fresh\n issued credentials through the supported issuer before using `run-cancel`. Static credentials\n and caller generations must still meet the original lifecycle and grant checks. Never edit a\n credential to add the permission.\n- The new command requires a fresh Describe-derived instance and epoch bind. An old issuer lacks\n its independent request observer and inspect/commit path, so a new signerless manager cannot use\n that issuer for cancellation. Do not treat missing support as a reason to bypass the issuer.\n\n### Order for a split topology\n\nSchedule a maintenance window for the version change; this is not a rolling-compatibility promise.\nTake the deployment's usual consistent backup of broker state, including run records, admissions\nand journals, and the issuer's private configuration. Upgrade the issuer, then the manager and its\nruntime dependencies to the same release, using their normal stop/start procedure. Reinstalling a\npackage does not replace modules already loaded by a running process. Reconnect callers through\nthe upgraded issuer and resolve the manager's current contract before invoking cancellation.\nThere is no broker format migration for this command and no automatic cancellation during upgrade.\n\n### What the result establishes\n\nA durable marker denies future admission but is not proof of cessation. Only `stopped` with the\ncaptured drive outcome establishes that local drive's result. If the holder recheck fails or the\nrequest budget expires, a parked effect can remain running beside a committed marker. Preserve an\nunknown result; do not blindly resend it or declare cleanup complete. To continue cancellation,\na caller can make a fresh explicit request, which observes the marker and current holder before\nreporting its own result. Pending cancellation intent can also block a local launch after a definite\nno-dispatch refusal if marker absence was not confirmed within the budget.\n\n`run show` now displays revocation independently of recorded state. An unreadable marker yields\na partial result and exit 4 in human and JSON output, rather than an apparently complete view.\nAutomation must handle that existing partial-result convention. None of these observations proves\nmodel or tool preemption, physical process cleanup or cessation on another host.\n\n## Service authority status selector in 0.92.0\n\nA service endpoint authority status query that reads one operation now names that operation:\n`target: { kind: \"operation\", operation, requestId }`, where `operation` is `register`,\n`activate`, `renew`, `ready` or `revoke`. `@cotal-ai/core` exports the operation names as\n`ServiceStatusOperation` and the whole selector as `ServiceStatusTarget`. The 0.91.0 selector,\n`{ kind: \"operation\", requestId }`, no longer parses. The change is in `parseServiceAuthorityRequest`,\n`parseServiceAuthorityEnvelope` and the `ServiceAuthorityRequest` type, so it reaches every client\nand server built on them.\n\nA `revoke` operation status parses, but the 0.92.0 auth service does not answer it yet: it is\n`denied` with reason `unsupported` (HTTP 422), as SPEC states. To follow a revocation, read\n`target: { kind: \"instance\" }`, which reports `revoking` with the revoke's operation id.\n\n### What keeps working\n\n- Every running mesh. 0.91.0 shipped the request types and the client (the auth provider's\n `serviceEndpointAuthority` hook and its HTTP client), but no 0.91.0 auth service serves\n `/service-endpoint-authority`, so no deployment has status traffic on this route to lose. In\n 0.92.0 only a context opened with `serviceEndpoints` serves the route. That option is off by\n default and has no CLI flag; without it the route answers 404.\n- `target: { kind: \"instance\" }` is unchanged, and so are the register, activate, renew, ready and\n revoke requests themselves.\n- No credential, broker or connection is involved. Nothing needs reminting and no broker restarts.\n\n### What does not migrate\n\n- **Code that builds an operation status query without `operation`.** The 0.92.0 provider hook and\n client parse each request before sending it, so the query fails locally with a `bad-request`\n error, `service-endpoint authority: <detail>`, whose detail is\n `object has missing or unsupported fields`, and nothing is sent. An operation name outside the\n five gets the detail `unsupported status operation`.\n- **A 0.91.0 client still sends the old selector.** A 0.92.0 auth service opened with\n `serviceEndpoints` answers it with HTTP 400, whose body carries the same parser message. The\n service reads and writes nothing for it and logs nothing. The 0.91.0 provider hook and client\n never show that message: they cannot parse the 400 body as a protocol result, so they throw a\n `ServiceAuthorityTransportError` whose message is\n `service authority returned an unconfirmed response; retain the existing request identity`, with\n `httpStatus` 400 and outcome `not-executed`. A status query is a read, so nothing ran and\n nothing needs replaying. An auth service without `serviceEndpoints` answers 404 before it reads\n the body.\n- **No legacy operation is inferred.** Neither the client nor the service maps an old selector onto\n `register` or any other operation. One requestId can key a different request under each\n operation, so a guessed operation could answer with another request's status. Send the operation\n the request was made as; the requestId you stored with it stays the same.\n\n### Durable data\n\nNothing stored changes on upgrade, and nothing is rewritten. The selector is a request shape and is\nnever stored. 0.91.0 wrote no service authority request intents or operation records, so there is\nnothing to convert. A 0.92.0 context opened with `serviceEndpoints` writes its own records from the\nmoment it is enabled: an admission record when it first opens, then request intents, the\nper-instance intent index, operation, proposal and assignment records, and endpoint gates. Each\nintent already records its operation. Without `serviceEndpoints` it writes none of them.\n\n### Order for a split topology\n\nUpgrade every client that builds status queries together with, or before, the first auth service\nyou open with `serviceEndpoints`. That means code using `ServiceAuthorityRequest` or\n`parseServiceAuthorityRequest`, and every process that calls the auth provider's\n`serviceEndpointAuthority`. This change adds no ordering for brokers, managers or connectors;\norder those as their own sections say.\n\nThere is no compatibility window to schedule. A 0.92.0 auth service never accepts the old selector,\nand a 0.91.0 auth service has no handler for the route at all, so a mixed pool is not partly\nworking: every 0.92.0 auth service opened with `serviceEndpoints` refuses an old client's\noperation status queries with 400, which that client reports as the unconfirmed-response error\nabove, and one without `serviceEndpoints` answers 404. Nothing else is interrupted, so the change\nhas no outage window of its own.\n\n### Before the upgrade\n\n- Nothing needs a snapshot for this change, because it writes nothing. If the same upgrade first\n enables `serviceEndpoints`, take the deployment's usual consistent backup of broker JetStream\n state and the auth host's private configuration before that step, because the service endpoint\n host then starts writing to the auth bucket.\n- Find the old selectors in your code, then name the operation in each:\n\n```sh\ngrep -rn 'kind: \"operation\"' --include='*.ts' --include='*.js' src/\n```\n\n```ts\n// 0.91.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", requestId } }\n// 0.92.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", operation: \"register\", requestId } }\n```\n\nWhat this section measured, and what it did not: the local refusal and the accepted and refused\nselectors are covered by the core parser's own test. The 400, 404 and `unsupported` answers, and\nthe absence of a log line, are read from the auth service's source. The 0.91.0 client's error is\nread from the 0.91.0 client and core parser source; the client file is unchanged since 0.91.0. No\nmixed-version deployment was run.\n\n## History reader deletes after 0.86.0\n\nObserver and admin credentials now carry a `CONSUMER.DELETE` row for each of the connection's eight\nhistory-reader names, `hist_<readerId>_<n>`, on CHAT (and on DM for admin). `channelHistory`,\n`multiChannelHistory` and `dmHistory` read under those names and delete each reader when its step\nends. Before this, every read left its consumers for five minutes, and a dashboard polling history\nfilled the stream's consumer limit until reads were refused with \"maximum consumers limit reached\".\n\n### What stops working\n\nA static observer or admin credential minted by 0.86.0 or earlier has no reader-delete rows. On it,\na current client's history read fails with \"it predates the observer and admin reader-delete grant,\nso remint it\", and leaves one reader per stream until the broker's five-minute inactive threshold.\nA credential the CLI mints at start (`cotal console`, `cotal web` on a static auth mesh) is current\nafter the upgrade.\n\n### Before the upgrade\n\n- Remint every observer or admin credential a process reads from a file: `cotal auth mint <name> --profile\n observer` or `--profile admin`, and any `--creds` file given to a dashboard.\n- On a user-auth mesh, the auth service mints these views per connection. Upgrade it before, or with,\n the hosts that read history, so no current client connects through an older auth service.\n\n## Native key issuance recording after 0.85.0\n\nBefore upgrading, take a consistent backup of broker JetStream state and the auth host's private\nconfiguration and signing material through the deployment's supported backup procedure. The auth\nbucket must use file storage. Allow space for one issuance record per new callout exchange until\nits minted expiry plus clock tolerance; static credentials have no such record.\n\nEmbedders that call `startAuthCallout` must supply the required `recordIssuance` callback. It must\nfinish a durable create-only write for the broker-generated user key and recheck the validated\nlifecycle and credential authority before returning. A missing callback refuses startup; a failed\nwrite or recheck denies the exchange. Do not use the isolated tests' no-op callbacks in a deployment.\nThe stock `startAuthService` authority plane wires the recorder and its pruning automatically.\nExisting auth records need no manual rewrite, and no tenant or participant grant is added.\n\nFor split deployments, pause new managed launches and stop every old auth-callout responder before\nstarting the matched new auth service. A mixed responder pool can still issue keys without records.\nNew connections and reconnects cannot authenticate while the callout service is down. Existing\nbroker connections keep their current JWT expiry; this change needs no broker restart. Upgrade the\nCLI, Manager and connector packages together before admitting new managed launches, using the\nruntime's supported Manager custody or recovery path. This source change does not promise that\nrunning seats survive a Manager restart.\n\nThe new service prunes expired issuance rows before opening and on a timer. A corrupt row refuses\nopening or fences the running authority plane; investigate the stored evidence through the normal\noperator recovery procedure. Do not erase the auth bucket or recreate signing keys to clear it.\nRows written before a later authority-check failure can remain until pruning; they do not prove\nthat a client received a JWT or established a connection.\n\nConnections admitted before the last old responder stopped, static JWTs and old responders have no\nissuance record. They are not backfilled or identified from names. Bearers without current lifecycle\nand credential claims cannot be recorded and are refused; use the supported enrollment or renewal\npath for the selected release. These records alone do not provide persistent native-key cutoff or\ncomplete revocation. Reintroducing an old responder also reopens the recording gap.\n\n## Native Claude MCP discovery after 0.82.2\n\nBefore upgrading, snapshot Cotal configuration and the Claude profile on each launch host.\nOrdinary Claude spawns now load the host's native MCP sources alongside declared Cotal overrides.\nAn empty sharing map no longer suppresses native servers. Audit enabled servers and their\ncredentials before admitting new seats. Use an explicit sharing selection, including\n`--share-tools none`, to retain isolation. Existing sessions keep their loaded configuration.\n\nNo broker restart, credential-format migration or cross-host profile copy is required. Upgrade\nthe CLI, core, manager and connector together on each launch host. Pause new managed spawns\nwhile adopting the matched packages; use the runtime's supported manager custody or recovery\npath. Any interruption is that manager transition, not a change to the broker wire contract.\nCarried private homes stay isolated and do not import the operator's native profile.\n\n## Connector-store protection after 0.82.2\n\nSnapshot the private Cotal state, connector store and mesh configuration before upgrading.\nInstall the new CLI on each CLI and manager host, then verify the selected executable before\nrunning setup or store maintenance. The POSIX installer prints its absolute launcher because\nit cannot change the parent shell's command cache, aliases or functions. An older executable\nalready on disk remains unsafe for a newer store; do not use its force advice to recover.\n\nForward setup and connector seeding migrate built-ins automatically. Same-generation force and\nreset rebuilds remain supported. An executable older than the store now refuses every backward\nmigration, including force and reset. Select a verified executable at least as new as the store\nand run normal seeding instead. No credential or mesh-record format needs manual migration.\n\nA split deployment needs no broker restart for this change. Keep each host's CLI and connectors\non the same release. Pause new managed spawns while adopting it; a running manager keeps its\nloaded code until restarted. Use the chosen runtime's supported custody or preserve-state path.\nThe interruption covers that manager transition and depends on its runtime, not a broker outage.\nDo not overwrite a live manager installation or stop an unidentified listener to force adoption.\n\nAn implicit foreground spawn now chooses a numbered actor when the requested actor is occupied.\nExisting grants, launch material and sessions stay in place. An explicit name remains pinned\nand refuses an occupied actor rather than renaming it. Signed enrollments and managed handoffs\nkeep their existing identity semantics.\n\n## Background startup in 0.91.0\n\n`cotal up` now returns after background startup by default. `cotal up --detach` retains that\nmeaning. Use `cotal up --foreground` when terminal lifetime should own the native stack.\n\nA systemd unit, launchd KeepAlive job, container entrypoint or script that supervises the `up`\nprocess must add `--foreground` before upgrading.\n[Persisted callers](#persisted-callers-in-0910) gives the literal\nreplacement for each.\n\nA project folder an earlier release started keeps its own launcher after the upgrade. Stop it with\n`cotal down` as usual, upgrade, then run plain `cotal up` in that folder: it starts on the retained\nlauncher with the same identity, store and history. It does not need `space create` or `space add`.\n\nRecorded recovery requests retain their original foreground/background intent. They do not\nreinterpret a prior request using the new default. This change does not migrate a whole stack to\nowned services.\n\nPrepared plain-loopback user-auth roots can use the controller's broker, auth service, delivery and\nmanager members. First enable may return a known partial result after the first three start: the\nmanager needs an existing signed-in, granted CLI login. Sign in and obtain that grant, then run\nbare `cotal up`; no static caller or new grant is substituted. Auth endpoints come from the\ncreation-pinned child's native provider readiness, not a guessed URL. The controller keeps unknown\noperations blocked. Existing three-member auth/open journals are read with an unattempted auth row\nand written with all four roles; old clients do not understand that new record shape. Upgrade the\ncomposed CLI together before using it. TLS-required, remote and public-exchange user-auth paths\nretain their native route.\n\n## Persisted callers in 0.91.0\n\nSome command lines outlive the install that wrote them: OS units, scripts and container commands.\nAfter `npm i -g cotal-ai@<new>` they reach the new binary before any `cotal` command has run. This\nsection says what each one does after the upgrade and what to change before it.\n\n### Command mapping\n\n| Earlier command | Command in 0.91.0 |\n|---|---|\n| `meshes`, `meshes add` | `space`, `space add <name>` |\n| `clean store`, `clean all` | `reset store <space>`, `reset space <space>` |\n| `mint <name>` | `auth mint <name>` |\n| `mint --signer` | `auth signer --out <private-file>` |\n| `actor list`, `actor grant <actor>`, `actor revoke <actor>` | `auth grants`, `auth grant <actor>`, `auth revoke <actor>` |\n| `join [channel]` | `chat [channel]` |\n| `console` | `watch` |\n| `channels` | `channel` |\n| `send dm <agent>`, `send msg <channel>`, `send ask <role>` | `send -t <agent>`, `send -c <channel>`, `send -r <role>` |\n| `personas` | `persona` |\n| `endpoints`, `describe`, `invoke` | `endpoint`, `endpoint describe <target>`, `endpoint invoke <target> <command>` |\n| `deregister-instance` | `endpoint deregister <class> <instance-id>` |\n| `reconcile-gate`, `seats` | `manager reconcile`, `manager seats` |\n| `topology view` | `team check <file> --graph` |\n| `down -f <manifest>` | `team remove <deployment-id>` |\n| `status --components` | `service` |\n| `run journal`, `run migrate`, `run ps` | `run show`, `run check`, `run` |\n| `deliver pending <name>` | `pending <agent>` |\n| `web` | `service add <name> --preset web`, then `service start <name>` |\n| The four daemon entries | Their service presets, under [deprecated daemon entries](#deprecated-daemon-entries) |\n\nThe sections on this page for each command family give the details of its change.\n\n### What stops working\n\n`cotal up` returns after background startup by default. An earlier line such as\n`cotal up --space <space> --server <url>` that a supervisor runs as its main process now exits\nonce the stack is started. systemd then treats a `Type=simple` unit as finished and can stop the\nprocesses that unit started, and a restart policy starts another attempt. A launchd `KeepAlive`\njob relaunches it the same way. Add `--foreground` before upgrading. For a systemd unit, replace\n`ExecStart=/usr/bin/cotal up --space <space> --server <url>` with:\n\n```ini\n[Service]\nType=simple\nExecStart=/usr/bin/cotal up --foreground --space <space> --server <url>\n```\n\nFor a launchd job, add `--foreground` after `up` in `ProgramArguments`:\n\n```xml\n<key>ProgramArguments</key>\n<array>\n <string>/usr/local/bin/cotal</string>\n <string>up</string>\n <string>--foreground</string>\n <string>--space</string>\n <string>main</string>\n <string>--server</string>\n <string>nats://127.0.0.1:4222</string>\n</array>\n```\n\nA container command or script makes the same change to its `up` line. When background `up` is the\nmain process of a systemd unit, it prints a warning naming the unit and the `--foreground` remedy\nafter it starts or refreshes the stack. `up -f`, which has no foreground form, and a dry run print\nnothing. A launchd job gets no warning. `cotal up --detach` keeps its meaning. The warning changes\nno mode and no unit, and no unit or script is rewritten or restarted.\n\n`cotal spawn <name>` now launches through a manager and attaches. A container or script that ran\nan agent directly with `spawn <name>` adds `--foreground`.\n\n### Deprecated daemon entries\n\n`supervise`, `deliver`, `auth-service` and `feedback-intake` stay accepted with their existing\nflags for every caller: units Cotal installed, units and scripts you wrote, and container commands.\nThey are deprecated and are removed in `1.0.0`, one release for all four. Help and completion no\nlonger list them. Each run prints one line on stderr naming its replacement and that release.\nStdout, behavior and exit codes are unchanged, so no unit restarts in a loop because of the\nupgrade. `deliver pending` warns the same way and names `cotal pending <agent>`.\n\n| Entry | Replacement |\n|---|---|\n| `supervise` | `cotal service add <name> --preset manager` |\n| `deliver` | `cotal service add <name> --preset delivery` |\n| `auth-service` | `cotal service add <name> --preset auth-service` |\n| `feedback-intake` | `cotal service add <name> --preset feedback-intake` |\n\n`cotal service add <name> --preset <preset> --help` lists the options a preset takes, and\n`cotal service start <name>` starts it. A `supervise --roster <file>` line maps to the manager\npreset's `--roster <file>`, with an absolute path, and `cotal service start <name> --foreground`\nkeeps the manager in the foreground as `supervise` did. The manager, delivery daemon and auth service that `up`\nstarts itself do not warn, so the line in a log always comes from a unit, script or container you\ncan change.\n\n### Owned units\n\nA manager unit that an earlier `cotal service install` wrote keeps calling `supervise`. After the\nupgrade it starts the new binary and prints the deprecation warning on each start. This release\ndoes not rewrite that unit.\n\nA unit that this release's `cotal service install <name>` writes runs the hidden `__service-unit`\nentry, which an earlier binary does not have. Before you downgrade, stop it with\n`cotal service stop <name>` and remove it with `cotal service uninstall <name>`. After the\ndowngrade, install the unit again with the earlier binary's `service install`.\n\n### Containers\n\nThe `solo` service in `deploy/docker/compose.yaml` runs `spawn scout --foreground`. The team\nservice and the image's default command still run `supervise` with `--roster`, so their logs show\nthe deprecation warning. An image you already built carries its own CLI and is unaffected until\nyou rebuild it.\n\n### Before the upgrade\n\n- Add `--foreground` to every unit, launchd job, container command or script that supervises\n `cotal up`.\n- Add `--foreground` to every container command or script that runs `cotal spawn <name>` directly.\n- Move units and scripts that call `supervise`, `deliver`, `auth-service` or `feedback-intake` to\n their service presets before `1.0.0`.\n- Before a downgrade, stop and uninstall every unit this release's `service install` wrote.\n\n## Team commands in 0.91.0\n\nThe new grammar names fresh intent `team boot <file>` and additive intent\n`team deploy <file>`. `team check <file> --graph` resolves declared access\nwithout a broker. `team` and `team show <id>` read local fresh summaries, additive ledgers and\nagent/channel returned receipts; `team remove <full-id>` is the ID-only additive\nteardown name. An edited manifest is not authority to remove an old deployment.\n\nBoot's `--dry-run` is a declaration view.\nDeploy's `--dry-run` reads native presence and channel registry for advisory\nclassification against a prior ledger. Remove's preview compares a legacy\nledger with native manager PS and display-only membership observations, or a\nreturned channel-only receipt with current KV revisions. No preview authorizes\na launch or stop. Bare `team boot <file>` enters guarded native fresh\nstartup only for a prepared, stopped local root; it refuses a leftover manager\nrecord and offers rich, JSON and plain receipts. User-auth boot records an intent before\nowner/trust preparation and retains uncertainty without retry. Windows boot\napply refuses, because protected service-record inventory is not supported there.\nBare `team deploy` applies all-new channel-only manifests with explicit live\ndelivery, or one to 100 all-new agents on a static/open mesh, optionally with\nup to 100 new live channel cards. A mixed deployment records one intent before\ncreating channels and launching agents through one pinned manager/runtime.\nEach channel revision and original launch request, allocation and matched join\nis recorded before continuing. A full receipt requires both sequences to match.\nUnknown effects are retained, not replayed; completed effects are not rolled back.\nBoth routes offer rich, JSON and plain output. Identical settled deployments can return a verified\nno-op without new effects. Named stale-agent waivers retain the old policy without restarting.\nUser-auth agent apply and existing-card mutation still refuse effects.\n\n`team remove <id>` can retire a returned static agent deployment through its original manager.\nReview `--dry-run` first; scripts require `--yes`. This needs the native slot query, original UIDs\nand native retired slot/head plus cleanup evidence, not just stop acknowledgements. A returned\nchannel-only deployment instead uses deletes conditioned on its original card revisions. A static\nmixed deployment retires agents first and needs a complete retirement receipt before checking and\ndeleting cards, under one confirmation and maintenance lock. Both receipts must exist before its\nblocker is released. Partial and unknown effects retain their journals without replay. History,\ngrants and readers are unchanged. Open/user-auth agent retirement and legacy removal still refuse.\nThe old `spawn -f` manifest spelling no longer deploys. Native `up -f` still\nstarts a fresh mesh; `down -f <file> --run <id>` remains the retained route\nonly for an older additive ledger. Move an automated agent deploy or teardown\nto the new spelling only when its auth mode and manifest are ones these routes accept.\n\n## Credential commands in 0.91.0\n\nCredential minting is `cotal auth mint <name>`. The old `cotal mint` spelling refuses before\nissuance and names its replacement. A stripped account signer is exported with the explicit\n`cotal auth signer --out <private-file>`, not `mint --signer`.\n\nThe default standing credential label is create-only. Repeated minting does not silently replace\nit. Choose a new label or a separate private output. An explicit existing output is replaced only\nwhen it is an owned private native credential matching the supplied `--identity`. Explicit\nexports cannot overwrite managed `.cotal` or workstation state.\n\nThe new receipt reports the actual principal, resulting lifecycle, private destination and\nnative expiry without credentials. Reusing an nkey does not promise reuse of its old lifecycle\nor bind-only inboxes. An agent-profile re-mint reports its newly allocated UID and warns about\nthat distinction; consuming clients must use the returned UID. Provisioning is explicit and is\nnot evidence that a client joined. Scripts should parse the finite `--json` document rather than\nscraping the former text lines. Secret and signer files must stay out of support bundles.\n\n`auth mint <name> --endpoint <class> --out <private-file>` is a separate caller mode. It uses the\nsame native static authority and a live digest-verified untargeted synchronous ephemeral surface.\nTargeted, journaled or action commands refuse the whole issuance; no unsupported row is dropped.\nThe native agent footprint and exact additional endpoint rows are provisioned with required TLS,\nwithout adding spawn/run/admin scope or starting a seat. `--channels` enters both explicit channel\nACLs and defaults empty; `--expires-in` remains native seconds.\n\nThe create-only private `<out>.issuance.json` intent is retained before footprint work and contains\npublic identity/lifecycle/scope metadata only. It is not a native grant or completion receipt. An\nexisting output or intent blocks replay, including when an interrupted operation left no output.\nKeep that intent with any original file while diagnosing an unknown result. Provisioner close is\nreported separately. No raw-file caller identity, managed launch, user-auth issuer or environment-\nexport onboarding is added by writing this credential.\n\n## Endpoint deregistration in 0.91.0\n\nReplace `deregister-instance --endpoint <class> --instance <id>` with\n`endpoint deregister <class> <full-instance-id>`. Both positional values are required.\nThe old spelling refuses before effects and has no implicit instance selection. Review the\nnew `--dry-run` output, then use `--yes` in scripts. JSON retains native revision receipts;\nunknown or superseded deletions are not retried. User-auth and open targets do not receive\na static-signing fallback.\n\n## DM pending inspection in 0.91.0\n\nUse `cotal pending <agent>` for a one-document JSON/plain read of native DM\ndurable counts and bounded candidate IDs. `--durable <exact-dm-durable>` targets\nan older lifecycle directly. The retained `deliver pending` spelling still\nuses its earlier output shape; scripts must migrate to `result` fields rather\nthan assuming the old per-line format. Candidate IDs are not proof of\nrecipient delivery or complete message history. This route has only the\nstatic/open admin read authority; user-auth has no corresponding view yet.\n\n## Live space deletion in 0.91.0\n\n`space forget <name>` changes only the local registration. It does not delete\nlive broker state. `space delete <name> --dry-run` now reads known first-party\nnative streams, bucket backing streams and presence entries on the registered\nbroker in open or static-auth mode. Static transfer-stream names are read under\nan additional exact-name grant on a second confirmed connection. This non-atomic\nread is not an apply plan or evidence that presence records are live. User-auth\ninspection and deletion remain unavailable. Bare deletion now requires a typed\nexact-name confirmation on a terminal or the exact positional name with `--yes`\nin a script. It rereads the native impact and checks the current target before\nsubmitting exact stream deletes, stopping on the first refused or unconfirmed\nreply with acknowledged and remaining names. A native post-read reports any\nsurvivor or newly observed transfer target without retrying. No process, registration or local\nfile is removed.\n\n## Stopped reset in 0.91.0\n\n`reset store <space>` and `reset space <space>` are the named replacements for\n`clean store` and `clean all`. `reset store --dry-run` provides a partial\nlocal-impact preview of the store path, recorded processes, maintenance journal\nand broker credless reachability. `reset store` requires confirmation,\nacquires the maintenance lock, rechecks its exact store identity, legacy\nprocesses and all recorded service instances for that space, and calls the\nexisting store-only cleanup. Any recorded supervisor blocks reset. It keeps trust and the\nregistration. TLS-required or TLS-first store targets refuse, because no trusted\nstopped-broker check covers them. `reset space` now routes a\nreviewed local identity reset through native clean-all. It requires Linux, an\nisolated locally started registration and plain-NATS loopback nonresponse.\nIts preview inventories provider-declared keys and native filesystem targets;\napply rechecks the scope under the maintenance lock and constrains each delete\nto that reviewed inventory. Trust is removed last, followed by the unchanged\neligible registration. Unknown effects are not replayed. Service definitions,\npersonas, packages, IdP logins and remote data stay. Unsupported providers,\nforeign material, TLS-first and non-Linux targets refuse.\n\n## Backup maintenance in 0.91.0\n\n`backup create` supports one-document `--json` and plain reports. The completion receipt follows\nnative attempt cleanup. A published artifact with unconfirmed cleanup is partial; retain its\npath and original attempt instead of repeating creation. `backup inspect` reads metadata and\n`backup verify` checks declared file sizes and hashes. Neither proves restoration or authority\ncontinuity, starts a broker or writes a staging tree.\n\nThe canonical stale pre-commit recovery is `backup recover <full-attempt-id>`. Healthy retained\nold-source cleanup is `backup cleanup-fallback <full-attempt-id>`. Review `--dry-run`, then use\n`--yes` in scripts. Neither flag bypasses owner, deadline, root, journal revision or store-identity\nchecks. Restore startup stays on its guarded existing route.\n\nA controller-owned stack cannot be preserved in 0.91.0. On a root with a stack journal,\n`down --preserve-state`, `backup create`, `backup restore` and `up --restore` refuse `UNSUPPORTED`\nbefore any effect. Stop such a stack with plain `cotal down`; it keeps its store, channels and\nhistory for the next `cotal up`. Preserve, backup and restore of a controller-owned stack return in\na later release. A root without a stack journal keeps its preserve, backup and restore.\n\n## Feedback input in 0.91.0\n\nUse a protected `feedback --key-file <path>` rather than placing an intake key on argv.\nThe old `--key` spelling refuses without sending or echoing its value. Public feedback still\nworks with a contact email and requires no environment setup. JSON/plain output is one public\nreceipt, not a copy of the submitted body. Unknown submission outcomes are not retried, and no\nredirect or alternate credential/intake is substituted.\n\n## Credential diagnosis in 0.91.0\n\n`doctor auth` uses JSON/plain reports and distinguishes local claims from live authorization.\nA plain diagnostic that encounters material requiring migration refuses without moving it.\nExplicit `--fix` retains the native static daemon repair, with separate migration/re-sign receipts\nand no user-auth signer fallback. A local re-sign is not daemon adoption or broker acceptance.\nUnhealthy, partial and unknown results retain their classified outcomes; scripts must read the\nnew result and error fields instead of scraping the old `auth: healthy` line. Never respawn from\na locally ordered older credential file alone.\n\n## Service adapter declarations in 0.91.0\n\nA local-process provider can expose the version-1 workstation service ABI alongside its existing\nprocess metadata. It declares a closed option schema, explicit foreground/multi-instance/log\ncapabilities and launch/readiness hooks. Re-add an extension after adding this ABI so its cached\nservice metadata and package fingerprints are recorded. An older descriptor is not imported to\nguess its schema. Plain process definitions keep their existing meaning.\n\nPresets are declarative registry contributions. Their option flags, help, completion and guided\nquestions derive from the adapter schema. Re-add a preset provider to populate its new cache;\nmissing or changed metadata refuses without importing code to guess the definition.\n\nThe top-level `feedback-intake` launcher is a [deprecated entry](#deprecated-daemon-entries). For a\nnew intake, save a definition with\n`service add <name> --preset feedback-intake --space <s> --root <absolute-root> --keys <private-keys.json> --creds <private-creds>`, then use `service start <name>` in that space. An explicit\n`--store` must be an existing private file. The default is private and instance-keyed. No broker\nor other service is started, no signer is minted and required TLS is preserved. Native startup\nreceipts are private, request-specific observations rather than native endpoint serving claims.\n\nA service instance pins its adapter name, package version and declaration fingerprint. A later\npackage update does not silently adopt a running or retained instance under a different adapter.\nStop the exact owned process and explicitly remove its stopped records before choosing changed\nadapter metadata. Logs remain private local files. Extension removal refuses live, foreign or\nunresolved service instances, including records outside the current mesh selection. None of\nthese declarations migrate legacy PID records or supply OS-unit ownership. Native manager\nimpact comes from its explicit adapter's attributed current read, not cached metadata.\n\nThe delivery implementation now registers its service adapter and legacy custody descriptor.\nCustom composition roots that want those inventory/control rows import `@cotal-ai/delivery`.\nThe full binary already composes it. CLI stop helpers share the workspace descriptor without an\nimplementation-to-implementation import. The `delivery` preset requires the recorded root and\nexisting scoped credential, keeps the native store/account/lease guards and does not adopt an\nexisting legacy daemon. Its hidden child entry is recognized by the same process-role predicate.\nThe old `deliver` machine-facing route remains for the stack callers that still use it.\n\nFresh static-auth and open `team boot` now use controller-owned stack members and accept\n`--foreground`. Their manager receives a digest-pinned manifest launch spec, with no additive\nownership ledger. A recorded stack can resume only after a completed whole-stack stop with its\nmanager never attempted, recorded broker inputs and each member's settled native evidence. Legacy\ncustody, prior team boots, supervisors and unresolved attempts still refuse. User-auth preparation\nuses ordinary `up --user-auth`, login and an explicit grant, then `down` before `team boot`. No\ncaller or owner grant is substituted.\n\nNew controller journals record the broker store path, storage cap request and bind host. A later\npreparation keeps those inputs and refuses differing explicit values. Older journals remain\nreadable. If their broker is stopped, restart requires the original store named with `--store-dir`\nand the original `--max-file-store` and `--host` values, if set. The store must already be a real\ndirectory. Do not omit the cap or host unless the original start set neither. That restart records\nthe inputs; run `down` before the stopped-stack team route. No store path is inferred from an\nabsent field, and bare restart never silently opens a default empty store.\n\nThe native manager status contract adds optional caller-scoped stop-impact metadata and advances\nthe cluster revision. Upgrade the composed manager and its compatible registration host together;\na host that does not accept the current contract must refuse registration. Older managers can\nomit the observation. Component status then says impact was not reported, rather than inferring\nthat seats survive. An observed policy does not migrate PID records, reserve a stop or confirm\ncleanup. The manager package adds a separate controller-only `manager-service` adapter and\n`manager` preset. Existing legacy manager records are not migrated or adopted. Update the\ncomposed packages and extension metadata together; an old cached declaration is not rewritten\nbehind a retained instance. Native stops require a fresh full-scope read and unchanged request-bound\npolicy, with native returned evidence plus original process exit. Missing terminal evidence retains\nits reservation and blocks replacement, without force kill. Cleanup remains not checked.\nThe direct service route currently refuses foreground mode. On an open mesh there is no\nauthenticated caller, so readiness and stop impact come from the pinned child's own request-bound\nlocal receipt. Its registration must equal the launch identity bound to that PID. Status shows it\nas a child-reported local receipt, not an attributed endpoint response. OS installation and\nnon-Linux protected process support are not available.\nRecorded systemd user units with an original invocation ID use that invocation's native journal\nfor finite/follow logs. Missing invocation evidence refuses instead of reading a stale direct-child\nfile. Native cursors and timestamps are retained; journal retention/access is not proof of complete\noutput or completed service work. No unit install/control support is implied by this read route.\n\nAuth providers may implement the optional read-only `preflightServer` hook. It checks public\nserver inputs without creating a directory, pin, key, grant or daemon. The controller's user-auth\nroute requires it before preparation; providers without it refuse rather than skipping validation.\n`prepareServer` remains authoritative and checks again before provisioning.\n\nThe auth package also declares an `auth-service` preset over its prepared native key projection.\nIt does not migrate or reclaim existing PID records. Its real child claims custody before opening\nplanes and publishes a private startup phase only after native initialization and signal handlers.\nThis route declares shared foreground control and still refuses remote broker and TLS-required\ntargets. The transport refusal is\nintentional, because not every native authority connection carries the registered transport\nrequirement. No plaintext substitution or new issuer is used.\n\nManager maintenance now supports finite JSON/plain reports. Gate repair and seat drain default\nto cancellation in a terminal and require `--yes` in scripts. Use `manager reconcile --dry-run`\nor `manager seats --drain --dry-run` first. Gate repair requires one registered root/space/broker\nand carries its TLS requirement through native liveness/eviction. It does not dial a default broker\nwhen that target is missing. An interrupted submitted repair or unresolved drain can return exit 3;\nconfirmed native results remain distinct from unconfirmed effects. No process is started.\n\n## Linear reports in 0.91.0\n\nUpdate the installed Linear extension with the matching CLI/workspace generation. Account add/show,\ninventory and direct call/resource/prompt JSON now use version-1 envelopes: read the native\ninventory from `result.inventory` and a native request reply from `result.reply`. Rich/plain reports\nbound displayed rows and lines without truncating JSON. MCP tool `isError` remains a result with\nexit 0 and a warning, not a work-success claim. Native not-executed refusals stay distinct from\nunknown submissions, and neither is replayed automatically.\n\nThe `linear` service preset runs the same native endpoint implementation from an explicit static-auth\nroot and existing account. It retains native registration, renewal and bounded deregistration; no\nnew generic caller is issued. Its private phase proves handler initialization only, not a current\nendpoint reply or completed deregistration. Foreground service start uses the shared terminal\nlifetime and native signal handlers, not the old serve parser. User-auth/open targets and\nOS-supervised service operation refuse. The older `linear serve` route remains available for\ninstalled callers.\n\nA bare Linear/account group opens no account. Wrong-mode flags or extra operands refuse before\naccount access. Interactive OAuth remains a human flow without JSON. Foreground serve stays on its native human-output path.\nStatic caller issuance keeps its native authority and now returns shared JSON/plain private-file\nreceipts. Use an existing protected physical parent outside managed state; an existing output\nrefuses before provisioning. Uncertain provisioning/publication is not replayed and no environment-\nexport launch recipe is printed. The supported `auth mint --endpoint` form resolves its scope live;\ntargeted/journaled/action issuance and consuming launch adapters are not supported. These changes do\nnot replace ownership, start a service or print secrets.\n\n## Setup inputs in 0.91.0\n\n`cotal setup` takes four inputs: destination, application, project folder and space. Nothing on a\nrunning mesh changes, and there is no outage window.\n\n### What stops working\n\n- A `cotal setup --yes` or a setup with no terminal exits 1 with `USAGE_INVALID` unless it names\n every input as a flag. It no longer sets up every detected connector and seeds the folder it\n runs in.\n- Setup sets up one application, the one `--harness` names, and records it as the space's\n `harness` preference. An application whose executable is not on PATH exits 4 with the rest\n saved; it used to be skipped with a warning.\n- The default persona lands in `<project>/.cotal/agents`. Setup no longer looks for a running mesh\n to seed into.\n- A repeat `cotal setup` no longer prints a status card. `cotal status` reports the same state.\n\n### Before the upgrade\n\nA script or image that ran `cotal setup --yes` names the four inputs:\n\n```bash\ncotal setup --destination local --harness claude --project <dir> --space <name> --no-input\n```\n\nFor a space that runs elsewhere, register it first with `cotal space add`, or pass the bundle its\noperator exported with `--connection-file`, and use `--destination self-hosted` with the\nregistration's root as `--project`.\n\nStartup remains opt-in. `--start` conflicts with `--configure-only`; a script, JSON or plain\ninvocation must add `--detach`. `--yes` by itself still saves configuration only. Startup failure\nleaves completed preparation in the result. Do not treat an unknown startup result as permission\nto submit another launch; inspect the selected space and recorded original request first.\n\n## Reads that install nothing in 0.91.0\n\nA read no longer installs or refreshes the built-in connectors and the web dashboard before it\nruns. Nothing on a running mesh changes, and there is no outage window.\n\n### What stops working\n\n- On a fresh machine, `cotal status`, `cotal ext list`, `cotal ps`, `cotal runtimes`, `cotal docs`\n and the other reads leave the extension root empty. `cotal setup`, or any command that is not a\n read, installs the built-ins as before.\n- After an upgrade, a read does not refresh the built-ins. Where it would load one still at the\n older version, such as a connector's setup row in `cotal status`, it names the package and its\n version with `cotal ext seed` instead of loading it.\n\n### Before the upgrade\n\nA script that ran a read to install the built-ins, such as `cotal ext list` right after\n`npm i -g cotal-ai`, runs `cotal ext seed` instead.\n\n## Space selection in 0.91.0\n\n`cotal use` and the target a command picks without `--space` change. Nothing on a running mesh\nchanges, and there is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal use <space>` no longer checks the broker. It selects any registered space, including a\n stopped one, and the command that targets it reports the space not up. A name that is not\n registered exits 2 with `TARGET_NOT_FOUND` instead of 1. Bare `cotal use` reports the selection\n instead of exiting 1 with its usage, and `--json` writes one envelope.\n- A command run without `--space` stays on the selected space when its broker is down and refuses\n with `TARGET_UNAVAILABLE`, exit 2. It used to fall back to the one other running mesh and print\n a note that the default mesh was down. A selection that names a space no longer registered now\n refuses with `TARGET_NOT_FOUND` instead of falling back to the local project or the one running\n mesh.\n- `cotal space` lists the registered spaces. `cotal meshes` still lists them, and its `--json`\n lines keep their shape.\n\n### Before the upgrade\n\nA script that relied on the fallback names the space it means with `--space`, or selects it with\n`cotal use <space>`. A script that read `cotal use` exiting 1 for a stopped space checks the\nbroker with `cotal space show <space> --json` and reads `result.check`.\n\n## Extension listing output in 0.91.0\n\n`cotal ext --json` and `cotal ext list --json` now write one JSON document instead of one JSON line\nper extension. Nothing on a running mesh changes, and there is no outage window. Upgrading the\n`cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ext list --json` line by line reads one envelope instead. The rows it\n read are now `result.items`, each with the same fields, and `result.root` is the install prefix.\n With `jq`, `cotal ext list --json | jq -c '.result.items[]'` prints the rows as before. An empty\n prefix writes an envelope with no items instead of nothing.\n- A usage error from `cotal ext` exits 1 as one `refused` envelope under `--json`, and `--plain` or\n `--json` on `ext add`, `ext remove`, `ext root` or `ext seed` is refused. `cotal ext root` still\n prints the path alone.\n- The human listing is a table now. Human output is not a parsing target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ext list --json` and `cotal ext --json`, and change line-by-line reads to\nread `result.items` from the one document.\n\n## History reads in 0.91.0\n\n`cotal history [channel]` reads retained messages. Clearing is now `cotal history clear`, which asks\nin a terminal and takes `--yes` in a script.\nThis changes the CLI grammar only. Stored messages, credentials, agents and running services do not\nmigrate or restart, and there is no outage window. Upgrade the CLI and its first-party components\ntogether through the normal update path; a separate broker or manager can stay running.\n\n### What stops working\n\n- Replace a script's `cotal history --force` with `cotal history clear --yes`. `--force` is refused\n on every `history` form, `history clear` included, rather than clearing anything. Add `--dms` to\n the explicit clear command only when the purge should include direct messages and the identity\n holds that authority.\n- `history clear` accepts no channel argument, read filters or `--follow`. It purges every channel\n in the selected space; these inputs are refused rather than treated as a selective purge.\n- Bare `history` reads channels within the caller's read grant. It never clears stored messages.\n Use `--json` and `result.items` for scripts; the human report is not a parsing target.\n- A user-auth history read uses the connection's channel grant. DM audit history and stream-sequence\n cursors are not available through that ordinary human credential. Use a time with `--since`.\n\n### Before the upgrade\n\nFind scripts that pass `--force` to `history` and change their explicit purge invocation. No stored\nstate or credential backup is needed for this CLI-only transition. Keep the deployment's usual\nbackup procedure before an intentional history purge; reads do not make a backup or restore data\nthat retention has already removed.\n\n## Status output in 0.91.0\n\n`cotal status` writes one structured document with `--json`, takes `--plain` and `--all`, and exits by\nthe structured outcome table. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal status` exits 4 when it could not read part of what it reports, such as a PID record, the\n folder's trust material, a persona catalog, the mesh registry or one of its records, or a user-auth login. It exited\n 0. An unreachable broker, a stopped process\n or an unbound responder still exits 0.\n- The recorded meshes list checks only the selected mesh and reads `not checked` for the others. Pass\n `--all` to check every one.\n- The report no longer lists the roster or prints a `cotal status` title line, and its standalone\n hints moved to a `Hints` section at the end. Rows are labeled `name: value`. Human output is not a\n parsing target; use `--json`.\n- `--components` is refused with `--json`, `--plain` or `--all`. Its exit, `0` to `3` by component\n health, replaces the report's, so it does not exit 4 for a partial report.\n\n### Before the upgrade\n\nSearch scripts and monitors for `cotal status`. A check that treats any nonzero exit of bare\n`status` as an outage should treat 4 as a report with unreadable parts. A script that read the\nroster from `status` should use `cotal ps` for managed seats.\n\n## Read command output in 0.91.0\n\n`cotal ps --json` now writes one JSON document instead of one JSON line per seat, and `ps`, `models`\nand `runtimes` exit by the structured outcome table. Nothing on a running mesh changes, and there is\nno outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ps --json` line by line reads one envelope instead. The rows it read\n are now `result.items`, each still the manager's row unchanged, and `result.managers` names each\n manager instance and whether it answered. With `jq`, `cotal ps --json | jq -c '.result.items[]'`\n prints the rows as before. Instance headers no longer go to stderr under `--json`.\n- An incomplete manager census, where a registered manager did not answer, refused or serves a\n different contract, exits 4 instead of 1.\n- A mesh that `ps` or `models` cannot resolve or reach exits 2 instead of 1, and a space catalog\n refresh that `ps` or `models` needed and could not get exits 2 instead of 1. Under `--json` each is\n one `refused` envelope whose `error.code` names the reason.\n- `cotal models` exits 4 when a connector's catalog could not be read, and `cotal runtimes` exits 4\n when an installed runtime provider fails to load. Both exited 0. `cotal models --agent <name>`\n exits 5 when that one catalog could not be read. OpenCode 2.x, which has no catalog the CLI can\n list, is reported `supported: false` with the reason in `unavailable` and does not fail the read.\n- The human reports of `ps`, `models` and `runtimes` are tables now, and `ps --wide` prints one\n labeled record per seat instead of a facts line under each row. Human output is not a parsing\n target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ps --json`, and for checks of the exit status of `ps`, `models` or\n`runtimes`. Change line-by-line reads of `ps --json` to read `result.items` from the one document.\n\n## Shell completion script in 0.91.0\n\n`cotal completion <shell>` is now `cotal completion script <shell>`, and bare `cotal completion`\nreports which shells have the stub instead of printing usage. Nothing on a running mesh changes, and\nthere is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\nA startup line written by hand that runs the old form, such as `source <(cotal completion bash)` or\nthe PowerShell profile line `cotal completion powershell | Out-String | Invoke-Expression`, now\nloads no completion. The command exits 1, prints nothing on stdout, so the shell sources nothing,\nand names the replacement on stderr. Replace the line with its new form:\n\n```bash\nsource <(cotal completion script bash) # bash\nsource <(cotal completion script zsh) # zsh\ncotal completion script fish | source # fish\ncotal completion script powershell | Out-String | Invoke-Expression # PowerShell $PROFILE\n```\n\nA cached stub from an earlier release keeps calling the hidden `__complete` entry. The new ownership-aware\ninstaller does not adopt it just because its text matches a generated script. Review unrecorded\nstubs and custom startup wiring manually before replacing them. Nothing automatically rewrites\na hand-edited startup file. On Linux and macOS, `completion install powershell` asks the\ninstalled shell for its current-user all-hosts profile before adding an owned block;\nit does not adopt an existing manual line. Windows profile editing still requires a\nmanual review and the printed script.\n\nNew installs record their stub hash and startup block. Reinstall and update refresh unchanged\nrecorded stubs at the same path. Update adds no startup block and does not restore a stub the user\nremoved. Uninstall removes only unchanged recorded material. Modified or unowned material is\npreserved with a diagnosis. Reopening the shell is necessary to stop using functions already\nloaded in its parent process.\n\n## CLI command resolution in 0.91.0\n\nThe CLI now resolves and validates a command before it seeds connectors or writes configuration.\nNothing on a running mesh changes. Brokers, managers, seats, credentials and mesh records need no\nmigration and no restart, and there is no outage window. Upgrading the `cotal` binary is the whole\noperation. What changes is what two kinds of command line do.\n\n### What stops working\n\n- `cotal -v` no longer prints the version. It exits 1 and names `cotal version` and `cotal -V`. A\n script that reads the version this way must use `cotal version`, `cotal --version` or `cotal -V`.\n- The words `sp`, `a`, `s`, `r`, `x` and `h` are now aliases of built-in commands (`spawn`,\n `attach`, `status`, `run`, `ext` and `history`). An installed extension that provides a command\n under one of those words still loads, but the built-in wins and every run prints a warning naming\n the extension. Remove or update that extension to clear it. `cotal ext add` refuses a new\n extension whose name or alias is already claimed.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal -v`, and run `cotal ext list` to see whether an\ninstalled extension provides one of the words above. There is nothing to snapshot. The extension\nmanifest an older binary wrote is read as before.\n\n## Messaging commands in 0.91.0\n\n`cotal channels` is now `cotal channel`, and `cotal send` names its destination with a flag instead\nof a `dm`, `msg` or `ask` word. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal channels`, with or without `list`, `set` or `default`, exits 1 as an unknown command. Bare\n `cotal channel` lists the channels, `cotal channel set <name>` saves one channel's settings, and\n `cotal channel defaults` reads or updates the space-wide defaults.\n- `cotal send dm <agent> \"<text>\"`, `cotal send msg <channel> \"<text>\"` and\n `cotal send ask <role> \"<text>\"` exit 1 with `USAGE_INVALID`, because they name no destination\n flag. Use `cotal send -t <agent> \"<text>\"`, `cotal send -c <channel> \"<text>\"` and\n `cotal send -r <role> \"<text>\"`.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal channels` and `cotal send`, and change each line to the\nnew form above.\n\n## Managed Claude workspace trust in 0.80.0\n\nBefore upgrading, snapshot the manager's private Cotal state and Claude configuration home.\nPause new managed spawns while upgrading the connector, core, manager and chosen terminal\nruntime together. For tmux, cmux, Herdr, Orca and custodial Linux PTY seats, use that runtime's\nsupported manager-reload path and observe its custody/release bounds. Default `auto` selects\nin-process `pty`, whose seats cannot survive a manager restart. Drain those seats at an approved\nidle window, or use the documented preserve-state cut and `up` recovery, which resumes them\nrather than keeping their processes running. This upgrade does not require a broker restart.\n\nManaged launches now accept the authorized workspace through Claude's native startup dialog. They\nno longer require a prior interactive visit to that directory. Claude retains its normal\nrepository and linked-worktree trust semantics, including any trust it persists for later\nsessions. Foreground launches still prompt. No tool-permission bypass flag is added, but\naccepting workspace trust enables that checkout's hooks and settings, including its default\npermission mode unless a launch flag overrides it. Authorize only reviewed workspaces.\n\nThe launch recipe includes optional startup choices before its final confirmation. A custom\nruntime must implement `LaunchSpec.confirmBefore`, or refuse a recipe carrying it. Ignoring\nit leaves an untrusted launch at the native dialog until startup times out. The new launch\nrecipe does not require restarting an existing seat; manager reload continuity remains\nruntime-dependent as described above. Adopt the new runtime before launching new seats.\nNo credentials, mesh records or operator configuration files need manual migration.\n\n## AG-UI `takeCodePoints` export in 0.79.0\n\n`@cotal-ai/connector-core` no longer exports `takeCodePoints`. The AG-UI preview splitter is its\nonly user, and the package exported it only so the emitter could shorten a `RUN_ERROR` failure\ndetail, a path that never ran and is now removed. Nothing about a running mesh changes, and no\nshipped connector or host imports it. Only code of your own that imports it from the package is\naffected.\n\n### What stops working\n\nAn import of `takeCodePoints` from `@cotal-ai/connector-core` no longer compiles, and an ES module\nthat names it fails to link with `does not provide an export named 'takeCodePoints'`.\n\n### Before the upgrade\n\nTake a string's first `n` code points with `Array.from(s).slice(0, n).join(\"\")`, which is what the\nhelper did.\n\n## SecretStore create-only write in 0.78.0\n\n`SecretStore` from `@cotal-ai/core` has a fourth method, `create(key, value)`. It stores the value\nonly while the key is absent and resolves `true`, or resolves `false` and leaves the stored value in\nplace. The auth secret kinds made once per space now write their first value through it: the callout\naccount, the issuer keys, the owner secret and a hosted auth plane's instance identity. Before, two\nfirst calls on one space each made a value, the later `put` replaced the earlier one, and the earlier\ncaller kept using a value the store no longer held. The filesystem store implements `create` with an\nexclusive create, so a CLI root needs nothing, and stored values do not change.\n\n### What stops working\n\nA store of your own without `create` no longer compiles as a `SecretStore`. Plain JavaScript gets a\n`TypeError` at the first call that makes one of those kinds, such as `prepareServer` on a new space\nor the first `startAuthService` of a hosted context.\n\n### Before the upgrade\n\nAdd `create` to your store, decided in one atomic step by the backend, such as a conditional create\nor an insert that fails on an existing key. A read followed by a write brings the race back.\n\n## Manager-service authority refusal budgets in 0.78.0\n\n`POST /manager-service-authority` now counts its refusals against the exchange face's budgets, as\n`POST /exchange` does. On the loopback face a missing or wrong capability counts in the\ninvalid-capability window, and a refused IdP token or request counts in the refused-exchange window.\nOn the public face a refusal counts in the peer's bucket. A running mesh needs nothing, and a\nrequest that succeeds is never throttled.\n\n### What stops working\n\n`ManagerServiceAuthorityCtx` from `@cotal-ai/auth` now requires `failures` and `badCaps`, so a host\nthat builds it in an object literal without them fails to compile. The handler now calls the\n`peerKey`, `throttled` and `recordFailure` of the policy it is given. After 30 refusals in a minute\non one face, a further refusal on either route answers 429 without its reason.\n\n### Before the upgrade\n\nPass the `failures` and `badCaps` arrays your `/exchange` route counts in, so both routes share one\nbudget per face. A host that serves only this route can pass new empty arrays.\n\n## Primitive scope kind in 0.78.0\n\n`PrimitiveSpec` from `@cotal-ai/lang` replaces `opensScope: boolean` with `scope: ScopeKind | null`,\nthe scope kind a primitive opens, in the way `kind` names the effect it journals. Both engines now\nread that kind from the table. Before, they cast the primitive's name to `ScopeKind`, so a scope row\nmissing from the union compiled and reached the journal as a kind its readers did not know. A\nrunning mesh and its journals need nothing.\n\n### What stops working\n\nCode that reads `opensScope` from `PRIMITIVES` or a `PrimitiveSpec` fails to compile, and plain\nJavaScript reads `undefined` there, so a check on it never holds.\n\n### Before the upgrade\n\nRead `spec.scope !== null` where you read `spec.opensScope`, or `spec.scope` when you need the kind.\n\n## Token verifier clock tolerance in 0.77.0\n\n`validateUserToken`, and the `IdpConfig` that `verifyIdpToken` and `createIdpBridge` take, from\n`@cotal-ai/auth` no longer accept `clockToleranceSec`. Both verifiers allow a fixed 5 seconds of\nclock skew on `exp`, `nbf` and `iat`, the default they already applied. Nothing in the repository\npassed the option, so a running mesh behaves as before. Only code of your own that passes it is\naffected.\n\n### What stops working\n\nA call or `IdpConfig` that passes `clockToleranceSec` in an object literal no longer compiles. Plain\nJavaScript that keeps it still runs, and the value is ignored. A `NaN` value, such as `Number()` of\nan unset environment variable, used to turn off the expiry, not-before and issued-at checks in both\nverifiers, so an expired or post-dated token was accepted. A large value widened those checks by\nthat many seconds. Both kinds of token are now refused.\n\n### Before the upgrade\n\nRemove `clockToleranceSec` from each call and each `IdpConfig`. A host whose clock is more than 5\nseconds off the issuer's needs its time synchronized.\n\n## Auth context closure in 0.71.0 (unreleased)\n\nExisting deployments need no credential migration or restart for these additive APIs. Embedded\nhosts can now inspect `handle.connections()` and await `handle.closed` after `close()` or `drain()`\nto prove every owned transport ended, including the callout, replaced readiness readers and\nshort-lived clients. The inventory is a detached snapshot.\n\nA transport close failure now rejects with its connection label. The terminal signal stays pending\nwhile any connection remains live. Repair the failure and retry `close()` before awaiting\n`handle.closed`. Closing one hosted context does not close another account's context.\n\nRead a space's claim with `readPlaneClaim(kv, space)` on that account's leader-only auth bucket.\nAn unclaimed space returns `undefined`; held and released rows retain their claim identity.\nDeleted, malformed and foreign-space rows refuse. `PlaneClaimRow` and `PLANE_CLAIM_KEY` are exported.\n\nUse `observeAccountLivenessWithCreds({ servers, observerCreds, accountId, options })` with the\naccount-scoped membership-observer credential to list that account's connections. It never widens\ncredentials or evicts connections. Zero rows prove absence only with a complete sweep and the\nsingle-server proof. An embedded endpoint's trusted composition can retain transport custody\nthrough `EndpointOptions.onConnection`.\n\n## Unreleased\n\nOn a per-user-auth mesh, a spawn-scoped caller can arm the event plane of a child under its own\nowner without `admin`. This fixes owned spawns in spaces whose registration policy requires the\nplane. Upgrade the manager to pick up the admission change. No credential or state migration is\nneeded. Cross-owner arming still requires `admin`, and the child's own-channel rule and ledger\nenvelope are unchanged. A silent non-owner caller in a space without the policy still has the\nplane disarmed, with a notice if provisioning succeeds.\n\n## Hosted auth plane identity in the store (unreleased)\n\nA hosted context started through `startAuthService` keeps its auth plane instance identity in the\ninjected `SecretStore` under `authInstanceKey(space)`, with the other auth secret kinds. It used to\nsit under `stateDir` at `.cotal/space.<hex>/auth-instance.json`, though `stateDir` holds non-secret\nstate and the record holds the plane's private serve seed. The first start of an upgraded context\nputs that record into the store, removes the file and keeps the instance. A CLI root is unchanged.\n\n### What stops working\n\nA start refuses when the store and `stateDir` hold different instance identities, and names both. A\nstore that refuses a put of the new key fails the start.\n\n### Before the upgrade\n\nLet the store accept a put of `authInstanceKey(space)`. A copy of `stateDir` taken before the upgrade\nstill holds the serve seed, so delete it or protect it as secret material.\n\n## Injected SecretStore naming in 0.76.0\n\nThe manager and the delivery daemon name an injected `SecretStore` through\n`injectedSecretStoreIdentity` from `@cotal-ai/core`. The rule is unchanged: the store's declared\nidentity, else the coordinate in `COTAL_SECRET_STORE`, else a refusal. A running mesh needs nothing.\n\n### What stops working\n\n`reloadStoreIdentityOf` from `@cotal-ai/delivery` takes `{ injected: true, store }` or\n`{ injected: false, identity }`. A call that passes `injected: true` with no store fails to compile,\nand plain JavaScript gets a `TypeError`. Both processes refuse an unnamed injected store with one\nmessage, which starts `an injected SecretStore must declare its identity`, so a log match on either\nold message no longer matches.\n\n### Before the upgrade\n\nPass the injected store to `reloadStoreIdentityOf`, or call `injectedSecretStoreIdentity(store)`.\n\n## Manager-service authority policy flag in 0.75.0\n\n`handleManagerServiceAuthority` from `@cotal-ai/auth` no longer reads `allowManagerAuthority` from\nits policy argument, and the field is removed. Both exchange faces set it to `true`, so it never\nrefused anything. A running mesh needs nothing: the public listener serves\n`POST /manager-service-authority` as it did before, and the docs now list that route.\n\n### What stops working\n\nA host that serves the route itself and passes a policy object literal with\n`allowManagerAuthority` fails to compile with TS2353. Plain JavaScript that set it to `false` got a\n403 from the handler, and its requests now reach the capability and IdP checks.\n\n### Before the upgrade\n\nRemove `allowManagerAuthority` from the policy you pass. Where you set it to `false`, leave the\nroute out of that listener's route table instead.\n\n## Hermes model from the environment in 0.68.0\n\nA connector now launches on the model and variant its launcher resolved (the `--model` or\n`--variant` flag, else the agent file's `model:` or `variant:`) and no longer reads them again from\nthe agent file. The Hermes connector also no longer takes a model from `HERMES_MODEL` in the\nenvironment of the process that spawns the seat, including when `spawn.env` lists it.\n\n### What stops working\n\nA Hermes spawn whose only model was `HERMES_MODEL` in the spawning environment is refused at launch,\nand the refusal names both ways to set a model. Spawns that set `--model` or `model:` are unchanged,\non every connector.\n\nCode that calls a connector's `buildLaunch` directly with only `configPath` now gets no model or\nvariant from that file. Pass them as `model` and `variant`.\n\n### Before the upgrade\n\nMove each Hermes seat's model from `HERMES_MODEL` onto its spawn with `--model`, or into its\npersona's `model:`.\n\n## Run answers on a participant manager in 0.68.0\n\nA participant manager now asks its issuing host for an answering credential by naming the run and\nstep it answers. The host reads the pause's token off that run's journal and no longer accepts a\ntoken from the manager. Runs on a mesh with no participant manager are unaffected.\n\n### What stops working\n\nWhile a participant manager and its issuing host run different sides of this release, the host\nrefuses every `cotal run answer` and every amendment that manager serves, because each side refuses\nthe other's request shape. Starting, resuming and reading runs is unchanged. A pause stays waiting\nthrough the window, or follows its timeout if it has one.\n\n### Before the upgrade\n\nUpgrade the auth service and every participant manager registered with it in the same window, then\nanswer the pauses that waited.\n\n## Headless OpenCode handshake in 0.69.0\n\nWith `COTAL_SERVE_HEADLESS=1`, the OpenCode launcher's `[cotal-serve]` line on stdout now carries\nonly `port` and `session`. The server password no longer appears in it, and the 1.x TUI no longer\nreceives the password on its command line.\n\n### What stops working\n\nA headless host that read `password` from that line has no password, and the server refuses its\nrequests. Seats with a TUI, and headless seats that no host drives, are unaffected.\n\n### Before the upgrade\n\nHave each headless host mint a password and pass it to the launcher as `OPENCODE_SERVER_PASSWORD`,\nthen use it for basic auth as before. Without that variable the launcher mints its own.\n\n## Filesystem store identity in 0.69.0\n\nThe delivery daemon's answer to the manager's store check now names a filesystem store by its root\nand by a random `id` that the store records once in `store.id` inside its own directory:\n`.cotal/store.id` for a workspace root, or the directory of the file for `cotal deliver --creds\n<file>`. A manager no longer counts the daemon's store as its own because the two roots have the\nsame path. On a split whose broker host and manager host use one root path, the manager host now\nstays off the daemon-credential renewal lease, so `cotal doctor auth --fix` on the broker host can\nrenew the daemon credentials.\n\n### What stops working\n\nA manager and a delivery daemon on different sides of this release refuse each other's answer to\nthe store check. The manager then remints no daemon credential, and a manager that is booting does\nnot start. This is read from the code and was not measured across two releases. A\n`cotal deliver --creds <file>` whose directory is a read-only mount and holds no `store.id` stops at\nstart. So does a `--creds` file that is its directory's `store.id` under any name, and a `store.id`\nthat is a symbolic link or holds anything but a lowercase UUID.\n\n### Before the upgrade\n\nUpgrade the broker host and every manager host of a space in the same window. For a `--creds` file\non a read-only mount, add a regular `store.id` file beside it that holds a new lowercase UUID and no newline,\nas `node -e 'process.stdout.write(crypto.randomUUID())' > store.id` writes. Move a `--creds` file\nnamed or linked as `store.id` to a file of its own.\n\n## Detached spawns with `--share-tools` in 0.69.0\n\nThe manager's `spawn` operation now takes `shareTools` as a list of MCP server names. The CLI parses\n`--share-tools` into that list before it sends the request, and the manager cluster document moves\nto revision 22. A cut taken with `cotal down --preserve-state` before the upgrade still resumes: the\nmanager reads its `cotal-manager-resume/v1` inventory and writes new cuts as\n`cotal-manager-resume/v2`.\n\n### What stops working\n\nA CLI and a manager on different sides of this release refuse a detached spawn that passes\n`--share-tools`, because the CLI checks each request against the contract the manager serves. This\nis read from the code and was not measured across two releases. A detached spawn without the flag,\na foreground spawn and a roster entry are unaffected. A manager older than this release cannot\nresume a cut that this release took.\n\n### Before the upgrade\n\nUpgrade the CLI on every host that runs `cotal spawn --detach` in the same window as the managers\nit reaches.\n\n## Shared MCP server checks in 0.69.0\n\nThe cotal config reader now checks each server under `connectors.<name>.mcpServers` when it reads\nthe file, and refuses one that cannot launch as written, naming the file and the field. The rules\nare in [the config file](config.md#the-config-file).\n\n### What stops working\n\nA config file that holds such a server refuses every Claude spawn that reads it, including one with\n`--share-tools none`. Before, a field of the wrong type failed each Claude spawn that shared the\nserver with a `TypeError` that named neither the file nor the server, a spawn that did not share it\nlaunched, and a server with no `command` or `url` was passed to `claude`, which never started it.\nRead from the code and not measured: spawns on other connectors, a manager resume and the step of\n`cotal setup` that records the shared list read the same files, so each stops at the same refusal.\n\n### Before the upgrade\n\nCheck `connectors.<name>.mcpServers` in the operator-level config file and in each space's\n`.cotal/config.json`. Give each server a string `command`, or a `type` of `http`, `sse` or `ws` with\na string `url`. Write `args` as a list of strings and `env` and `headers` as objects of strings, or\nremove the server.\n\n## Remote manager family eviction in 0.69.0\n\nA remote manager registered through its host now asks the host to evict up to 256 holders of its\ncredential family in one maintenance request, and the host reads the family once for the whole set.\nBefore, a restart sent one request per holder and the host read the whole family for each one.\nMeshes with no remote manager are unaffected.\n\n### What stops working\n\nWhile a remote manager and its issuing host run different sides of this release, each side refuses\nthe other's eviction request shape. A restart whose credential family already has holders then fails\nat its eviction step and leaves the manager's registration gate frozen. A first start, a clean stop\nand the host's reconciliation of a foreign slot holder are unchanged.\n\n### Before the upgrade\n\nUpgrade the auth service and every remote manager registered with it in the same window. A manager\nthat restarted inside the window resumes its frozen registration on its next start once both sides\nrun this release.\n\n## AG-UI emitter holder hooks in 0.69.0\n\n`AguiEmitterHolder` from `@cotal-ai/connector-core` now takes its hooks as one named object after\nthe emitter factory: `new AguiEmitterHolder(startEmitter, { onError, onRunClosed, waitLive, runMeta })`.\nOnly `onError` is required. Nothing about a running mesh changes, and every shipped connector passes\nits hooks by name. Only a connector of your own that builds a holder is affected.\n\n### What stops working\n\nA holder built with positional hooks, such as `new AguiEmitterHolder(start, onError, onRunClosed)`,\nno longer compiles, because the constructor takes two arguments. Plain JavaScript that keeps the\npositional form still runs, but the holder calls none of its hooks, so a failure never reaches\n`onError`.\n\n### Before the upgrade\n\nPass each hook by name, for example `new AguiEmitterHolder(start, { onError, onRunClosed })`, and\ndrop any `undefined` that filled an earlier slot to reach a later hook.\n\n## Worker run failure type in 0.70.0\n\n`WorkerRunFailed`, the failed result of `runInWorker` in `@cotal-ai/lang`, is now a union on\n`class`: `released`, `held`, `effect`, `too-large`, `rejected` or `error`. A running mesh needs\nnothing, because the runtime host and the engine thread ship in the same install. A run on the\ncompiled engine whose program throws an object with `code: \"L5012\"` or `code: \"L5025\"` used to end\nreleased and now ends failed, as it does on the walker.\n\n### What stops working\n\nTypeScript code that reads `code`, `reason`, `step`, `pending`, `kind`, `detail` or `tooLarge` on a\n`WorkerRunFailed` it has not narrowed fails with TS2339. A released, held, too-large or rejected\nresult no longer carries `code`, so JavaScript that branched on `L5012`, `L5025`, `L5006` or\n`L5010` stops matching with no error. `tooLarge` is gone.\n\n### Before the upgrade\n\nBranch on `class` where such code read `code`: `released` for L5012, `held` for L5025, `too-large`\nfor L5006 and `rejected` for L5010. An `effect` or `error` result keeps its `code`. Once narrowed to\n`too-large`, a result carries the `stepKey`, `bytes` and `bound` that `tooLarge` held.\n\n## Remote manager request builder in 0.70.0\n\n`remoteManagerClient.remoteManagerAuthorityRequest` from `@cotal-ai/manager` now takes an\noperation's coordinates as one object, and `remoteManagerRegistrationProof` from `@cotal-ai/core`\ncomputes the proof from the manager's identity state instead of a request. Nothing about a running\nmesh changes: the proof digest and the request on the wire are the same, so a manager and a host on\ndifferent sides of this release still accept each other. Only code that builds remote manager\nrequests itself is affected, in TypeScript and in plain JavaScript.\n\n### What stops working\n\nA call that passes the registration proof, contract artifacts, session, retirement or transfer\nreader as positional arguments after the operation no longer compiles. A call that passes a request\nto `remoteManagerRegistrationProof` no longer compiles either, because the second argument now names\nthe lifecycle `lifecycleUid`, as the identity state does.\n\nPlain JavaScript runs both old calls without an error. The builder drops the positional coordinates,\nso the host refuses the request with `requires a sha256 registrationProof`. A proof computed from a\nrequest leaves out the lifecycle, so the host refuses a request that carries it as a proof mismatch.\n\n### Before the upgrade\n\nName the coordinates, for example\n`remoteManagerAuthorityRequest(state, \"cli\", \"retire\", { registrationProof, retirement })`.\nCompute the proof as `remoteManagerRegistrationProof(owner, state)`, adding the contract artifacts\nas a third argument for activation only. A host that recomputes the proof from a received request\npasses `{ space, instanceId, lifecycleUid: managerLifecycleUid, identities }` from that request.\n\n## Bearer validator lifetime cap in 0.70.0\n\n`validateUserToken` from `@cotal-ai/auth` no longer takes `maxTtlSec`. It caps a bearer's lifetime\nat the cap of the bearer's view, the same cap the issuer applies when it mints: 900 seconds, or 300\nfor a `transfer-writer` bearer. The auth callout never passed the option, so a running mesh behaves\nas before. Only code of your own that calls the validator with `maxTtlSec` is affected.\n\n### What stops working\n\nA call that passes `maxTtlSec` in an object literal no longer compiles. Plain JavaScript that keeps\nit still runs, and the value is ignored. A `NaN` value, such as `Number()` of an unset environment\nvariable, used to turn the lifetime check off and accept a bearer of any lifetime. That bearer is\nnow refused at its view's cap.\n\n### Before the upgrade\n\nRemove `maxTtlSec` from each call. A test that needs a bearer to expire sooner mints one with a\nshorter lifetime.\n\n## Persisted identity records in 0.70.0\n\nThe manager instance identity, the manager sibling identities, the auth plane instance identity and\na participant manager's remote authority state now share one reader and one first mint in\n`@cotal-ai/workspace`, exported as `claimIdentityRecord` with the nkey check `identityOf`. Each\nrecord is read as a regular file, must hold non-empty nkeys and is created exclusively, so\nconcurrent first starts of a participant manager on one root now settle on one identity where each\nused to keep its own. `saveManagerInstanceIdentity` and `saveAuthInstanceIdentity` are gone. A\nrunning mesh whose records are plain files needs nothing.\n\n### What stops working\n\nA manager instance, auth instance or remote authority record that is a symlink, a directory or any\nother non-regular entry is refused where it used to be followed. The manager, the auth plane and a\nparticipant manager fail to start on it, and `cotal reconcile-gate` and `cotal deregister-instance`\nrefuse it. Retirement already refused it. A remote authority record with an empty nkey id or seed\nis refused too. A first mint that loses its race and cannot read the winner now refuses with\n`identity-record-create-lost` in place of `manager-instance-identity-create-lost` or\n`auth-instance-identity-create-lost`. Code that imports either `save` function no longer compiles.\n\n### Before the upgrade\n\nReplace a symlinked identity record with a copy of the file it points to. Code that wrote a record\nwith a `save` function plants it with `createManagerInstanceIdentity` or\n`createAuthInstanceIdentity`, which create the record when it is absent and otherwise return the\nstored one unchanged. Nothing replaces an overwrite of a stored identity.\n\n## Manager instance in user credentials in 0.70.0\n\n`AuthProvider.userCredentials` from `@cotal-ai/core` no longer returns `managerInstanceId`. A\n`manager-caller` credential's manager instance is the signed `act.managerInstanceId` claim in its\nbearer, which the broker verifies and the CLI already used. The reference provider in\n`@cotal-ai/auth` stops copying the exchange response's field into its result, where nothing\ncompared it with the bearer. The exchange still answers with the field, so a running mesh behaves as\nbefore.\n\n### What stops working\n\nCode of your own that reads `managerInstanceId` from a `userCredentials` result no longer compiles,\nand plain JavaScript reads `undefined` there.\n\n### Before the upgrade\n\nRead the instance from the bearer's `act.managerInstanceId` claim.\n\n## Auth plane identity location in 0.70.0\n\nThe user-auth service keeps its instance identity in the root's `.cotal/space.<hex>/auth-instance.json`,\nbeside the manager's. It used to sit inside `.cotal/auth`, at\n`space.<hex>/.cotal/auth/auth-instance.<hex>.json`, so a copy of that folder carried it. The first\nstart of an upgraded root moves the record and keeps the instance. A hosted context started through\n`startAuthService` has its record moved the same way inside its `stateDir`.\n\n### What stops working\n\nCode that calls `openAuthAuthorityPlane` without the new `identityRoot` option no longer compiles. A\nstart that finds a record both in `.cotal/space.<hex>/` and at its older place refuses and names the\ntwo files. A start also refuses when the older place of the auth or manager identity holds a symlink,\na directory or anything else that is not a regular file. The manager used to skip a dangling symlink\nthere and mint a new identity.\n\n### Before the upgrade\n\nPass `identityRoot` to `openAuthAuthorityPlane`. When `dir` is a workspace root's user-auth state\ndir, `<root>/.cotal/auth/space.<hex>`, pass that root. A plane with no workspace root, as\n`startAuthService` runs, passes `dir` itself. Either keeps the identity the plane already has: on the\nfirst start it moves from `<dir>/.cotal/auth/` to `<identityRoot>/.cotal/space.<hex>/`. Never pass a\ndirectory inside `.cotal/auth`: the record would land in the folder an operator copies and travel\nwith it again.\n\nA copy of `.cotal/auth` taken from a root last run by an older Cotal carries that root's record.\nDelete `.cotal/auth/space.<hex>/.cotal/auth/auth-instance.<hex>.json` from the root you copied it to\nbefore the first `cotal up --user-auth` there.\n\n## Per-seat `COTAL_` names in `spawn.env` in 0.71.0\n\n`spawn.env` in the cotal config no longer forwards a `COTAL_` name the launcher sets for each seat,\nsuch as `COTAL_ROLE`, `COTAL_MODEL` or `COTAL_SUBSCRIBE`. Before, a seat launched with no value of\nits own took the spawning process's value and ran under that role, model or read set. The\nmachine-wide knobs a seat already receives, such as `COTAL_HOME`, may still be listed.\n\n### What stops working\n\nEvery spawn and resume under a config whose `spawn.env` lists such a name is refused before\nlaunch, and the refusal names the entry. Code that calls `launchEnv` from `@cotal-ai/connector-core`\nwith such a name in `envAllow` gets the same error.\n\n### Before the upgrade\n\nRemove those names from `spawn.env`. Give each seat its role, model and channels with `--role`,\n`--model` and `--subscribe`, or in its persona's `role:`, `model:` and `subscribe:`.\n\n## Role addresses in 0.71.0\n\nA role must be one `[A-Za-z0-9_-]` token. Before 0.71.0 any other spelling was rewritten into one:\n` probe ` reached the `probe` queue and `pro.be` reached `pro_be`, while the message kept the\nspelling sent. An anycast to `*` was accepted and stored where no holder reads it.\n\n### What stops working\n\nAn agent whose role is outside the token set no longer starts, however it is launched:\n`cotal join --role`, `cotal spawn --role`, an agent file's `role:`, `COTAL_ROLE` and an embedded\nendpoint's `card.role` are all refused before the agent joins.\n\nA send to such a role, or to `*`, through `cotal send ask`, `/anycast` or `cotal_anycast` is refused,\nand nothing is stored.\n\n`routeToken` is no longer exported from `@cotal-ai/core`. A role routes as spelled, so code that\nused it to name a role's queue uses the role itself, and `assertValidRole` checks one.\n\n### What migrates on its own\n\nEvery task queue. A `svc_<role>` durable was always named from the rewritten token, so its pending\nrequests and its holders carry over.\n\n### Before the upgrade\n\nRename each role outside the token set to the token it already routed to: remove the surrounding\nspaces and replace every other character outside the set with `_`. Rename it where the holder is\nlaunched and in every script or prompt that sends to it.\n\n## IdP URLs on `localhost` in 0.73.0\n\nThe IdP URL that `cotal login --idp` and `cotal up --user-auth --idp` take, the JWKS URL the auth\nservice pins from it, the space-catalog link and `cotal sync` now share one rule: `https://`, or\n`http://` on a loopback IP literal. `localhost` is a name, so it no longer counts: a hosts entry\nwould choose the IdP, and with it the keys the callout trusts. Every loopback literal now passes, so\n`http://127.0.0.2/api/auth` and `http://[::ffff:127.0.0.1]/api/auth` are accepted where they were\nrefused before. A JWKS URL with any scheme other than `https:` or `http:` is refused. A mesh whose\nIdP uses HTTPS is unaffected.\n\n### What stops working\n\nOn a mesh whose IdP is pinned at `http://localhost:<port>/...`, the auth service builds its key\nresolver from the pinned JWKS URL at start, and that resolver now refuses it with\n`JWKS origin must be https (or http on a loopback IP literal for dev)`.\n\n`cotal login` and `cotal logout` with `--idp http://localhost:<port>/...` are refused with\n`idp url must be https (or http on a loopback IP literal such as 127.0.0.1 for local dev)`, and so is\nevery command that reads a session cached under that URL.\n\n### Before the upgrade\n\nUse `127.0.0.1` (or `::1`) in place of `localhost`. In the space's `idp.json` under the mesh's\n`.cotal/auth`, change `url` and `jwksUri` to the literal spelling and leave `issuer` and `audience`\nas they are. Owners derive from the issuer, so existing grants keep matching. Change a manifest's\n`broker.idp` the same way, because `cotal up` refuses an `--idp` that differs from the pin. Then\nhave each person run `cotal login --idp http://127.0.0.1:<port>/api/auth` again, because sessions\nare cached under the URL.\n\n## Terminal layout `Pane.confirm` in 0.73.0\n\n`Pane.confirm` is removed from `@cotal-ai/core`, and the tmux and cmux terminal-layout providers no\nlonger press Enter in the panes they open. The flag carried no prompt text, so both providers\npressed Enter five times, one second apart: the first dialog a pane showed was answered with its\ndefault, and a prompt that never appeared went unreported. Nothing in Cotal sets it, so a running\nmesh needs nothing. Spawned agents are unaffected, because their runtimes match the startup prompt\nby its text.\n\n### What stops working\n\nA `Pane` object literal that sets `confirm` fails to compile with TS2353. Plain JavaScript that sets\nit runs without an error, and the pane stays at its prompt.\n\n### Before the upgrade\n\nRemove `confirm` from each `Pane`. Start a command that shows a startup prompt with the option that\nskips it, or answer the prompt in the pane.\n\n## Carrying a resumed Claude session to another host in 0.67.0\n\n`cotal spawn --resume <id> --detach --on <instance>` now carries a Claude session held on the\noperator's host to the target manager instance. Both sides need this release: an older manager does\nnot serve `transcript-receive`, and the CLI then stops with that manager's refusal instead of\nlaunching. The manager cluster document moves to revision 21, and the `ps` row's `resume` object\ngains `host` and `transferredAt`.\n\nA manager host that runs carried seats needs `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN` or a\ncloud provider selection in its environment, because each carried seat runs in its own Claude home\nwith no stored login. On an authenticated mesh the CLI mints the transfer writer from the space's\nsigning seed, so the carrying host needs that seed, as for any other operator command. On a user-auth\nmesh it exchanges the operator's login for a `transfer-writer` view instead, so the operator's grant\nneeds scope `admin`, and the auth service must run this release. A remote manager receives a carry once\nits host serves the manager-service `transferReader` operation. A seat launched without carrying,\nincluding any `--resume` whose id this host does not hold, is unchanged.\n\n## Lifecycle head type in 0.67.0\n\n`LifecycleMapping`, the type `parseLifecycleHead` returns, is now a union on `state`. Nothing about\na running mesh changes: heads that parsed before parse the same way, and the refusals are\nunchanged. Only TypeScript code that compiles against `@cotal-ai/core` is affected.\n\n### What stops working\n\nAn `interface` that extends `LifecycleMapping` fails with TS2312, because an interface cannot extend\na union. Code that builds a head in memory no longer compiles when the head is `retiring` without\nits `op`, or `active` or `retired` with one. The parser already refused those heads.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type ActiveMapping = LifecycleMapping & { state: \"active\" }`. A reader that has checked\n`state === \"retiring\"` reads `op` without a guard.\n\n## Issuance gate types in 0.67.0\n\n`EpGateRow` and `EndpointGateRow`, which `parseIssuanceGate` and `parseEndpointGate` return, and\n`EpGateState`, which an `EpIssuanceGate` or `EpIssuanceBarrier` returns from `observe`, are now\nunions on `state`. Nothing about a running mesh changes: gates that parsed before parse the same\nway, and the refusals are unchanged. Only TypeScript code that compiles against `@cotal-ai/core`\nis affected.\n\n### What stops working\n\nAn `interface` that extends one of these types fails with TS2312, because an interface cannot\nextend a union. Code that builds a gate in memory, such as a custom barrier's `observe`, no longer\ncompiles when the gate is `frozen` or `retired` without its `op`, or `open` with one. The gate\nparsers already refused those rows.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type CustomGateRow = EpGateRow & { custom: string }`.\n\n## Lifecycle-blocked refusals in 0.66.0\n\nA refusal that carries `ai.cotal.ep.lifecycle-blocked` now reports only the lifecycle state it\nread. Nothing about a running mesh changes. A client that branches on the detail must read the new\nfield.\n\n### What stops working\n\nA refusal raised at the issuance gate used to carry `headState` without reading the head:\n`retiring` for a frozen gate and `retired` for a retired one. It now carries `gateState`\n(`frozen` or `retired`) and no `headState`. A client that treats `headState: \"retired\"` as a\nburned uid, or `headState: \"retiring\"` as a retirement in flight, no longer matches those\nrefusals, and the `[lifecycle ...]` suffix on the error string changes the same way. A custom\nissuance barrier whose `observe` returns a frozen gate without a valid `op` (a string `opId` and\none of the four op kinds) is now refused as `internal` by `registerServiceInstance`.\n\n### Before the upgrade\n\nUpdate such a client to read `gateState` for a gate refusal and `blockedOp` for the operation that\nholds the gate. `headState` is present only when the refusal read the head, for example an\nactivation refused because the head is still retiring.\n\n## Workflow programs that bind `once` in 0.65.0\n\n`once` is now a scope of the workflow language, so it is a reserved name. A program that declares\nits own `once` binding (`const once = ...`, a parameter or a function named `once`) is refused at\nvalidation with L2002. Nothing else about a running mesh changes.\n\n### What stops working\n\nA run whose recorded program binds `once` cannot be resumed after the upgrade, because a resume\nvalidates the recorded program again. A new `cotal run start` of such a program is refused before\nanything is recorded.\n\n### Before the upgrade\n\nList the runs with `cotal run ps` and check each program that is still running or held for a\nbinding named `once`. Let those runs finish on the old version before you upgrade the manager, and\nrename the binding in the program before you start it again.\n\n## From 0.58.0 to 0.59.0\n\nEvery connector now publishes a failed run's `RUN_ERROR` on `events.<owner>.<actor>` with the fixed\nmessage `run failed` and no `code` or `rawEvent`. The error text and error kind a harness reports\ncan echo a prompt, a peer message or tool output, and that channel has a different read ACL. A\nreader that showed the message or branched on `code` gets neither after the upgrade. Where a\nconnector reports the error kind as the agent's presence condition, that is unchanged.\n\n### Settle pending event frames before the upgrade\n\nEach session's events are frozen in its event write-ahead log before they are published. A session\nrestarted on 0.59.0 whose log still holds an unacknowledged frame with an older `RUN_ERROR` does not\nrepublish it: its event emitter halts with `egress-run-error` and publishes nothing further for that\nsession. The broker may or may not already hold that frame, so the halt cannot settle it.\n\n1. Stop the seats cleanly on 0.58.0, with the broker still up.\n2. List the logs that still hold a pending frame. The logs live under the events state root\n (`COTAL_WORKSPACE_ROOT`). Empty output means there is nothing to settle.\n\n ```sh\n find \"$COTAL_WORKSPACE_ROOT/.cotal/events\" -name wal.json \\\n -exec jq -r 'select(.pending != null) | input_filename' {} +\n ```\n\n3. For each session listed, start it again on 0.58.0 while the broker is reachable, let it recover,\n stop it, and run step 2 again. Recovery publishes the frame as 0.58.0 would have, error text\n included, so it only finishes what 0.58.0 had already started.\n\n If that start halts with `cas-loss` instead, the agent's subject is no longer at the sequence this\n log expects, and no restart settles that log, on 0.58.0 or later. A lost acknowledgement is one\n cause: the broker stored the frame, so it and its error text are already on the channel, and every\n retry halts the same way because the stream checks the frozen expectation before it deduplicates.\n The halt message names the other causes, such as a second emitter for the same agent under a\n different state root, a restored stream or frontier record, or a purged channel. With those the\n pending frame may never have reached the broker, so a `cas-loss` does not tell you whether it\n landed. Find and stop any second writer and rule out a restored state first. Clearing the halt\n then means purging the agent's event channel and removing the agent's directory under the events\n state root whole (see [Event plane](connect-claude.md#event-plane)). That abandons the pending\n frame whether or not the broker has it, and the purge also drops the earlier frames of every\n session of that agent.\n4. Upgrade once step 2 prints nothing.\n\nIf a session halts with `egress-run-error` after the upgrade, go back to step 3 for that session on\n0.58.0. Do not edit or delete `wal.json` on its own to get past either halt: clearing the pending\nframe abandons that epoch, an event the broker never received is lost, and removing part of the\ndirectory leaves a state the next start refuses.\n\n## Explicit actor grants in 0.59.0\n\n`cotal actor grant` no longer fills an omitted ACL flag with its wide default. A grant names\n`--scope`, `--allow-subscribe` and `--allow-publish`, or passes `--full` to give the ones it leaves\noff their wide defaults (`spawn,role:default`, `>` read, `>` post). Any other grant is refused. The\nbreak is in the CLI on the machine that holds the actor ledger, the one that ran\n`cotal up --user-auth --idp <url>`. No stored row, credential or wire message changes.\n\n### What keeps working\n\nExisting actor ledger rows keep the authority they were granted, and their users and agents connect\nas before. `actor revoke`, `actor list` and a `grant` that names all three ACL flags behave as they\ndid on 0.58.0. Nothing on disk is converted.\n\n### What stops working\n\nA grant that leaves off any of the three flags without `--full` exits 1 with\n`refusing to grant \"<actor>\" with --scope, --allow-subscribe, --allow-publish left off`, naming the\nflags it is missing, and then prints both accepted forms. It writes no row and does not retire the\nactor's current lifecycle. An existing row stays as it was, and an actor granted for the first time\nstays out until the grant is run again. This includes the bare grant printed on 0.58.0 by\n`cotal login`, `cotal status`, `actor list` and the not-granted refusal. Look for it in provisioning\nscripts, onboarding runbooks and anything that pastes those hints.\n\n### Upgrade order\n\nChange the scripts before the ledger machine is upgraded, and make each grant name all three flags.\n0.58.0 and 0.59.0 both accept that form. To keep a wide row, write its defaults out:\n\n```sh\ncotal actor grant <actor> --sub <IdP subject> \\\n --scope spawn,role:default --allow-subscribe '>' --allow-publish '>'\n```\n\nSwitch to `--full` only once the ledger machine runs 0.59.0. 0.58.0 refuses it with\n`Unknown option '--full'` before it reads the ledger. Brokers, managers and participant machines\nneed nothing for this break, so their order is the one the section above gives.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused grant changes nothing. The\nexposure is a grant script that runs against 0.59.0 before it was changed: it fails and grants\nnothing.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. On the ledger machine, save the output\nof `cotal actor list` to compare rows after the changed scripts run, and list the scripts that call\n`cotal actor grant`.\n\n### The upgrade end to end\n\n```sh\n# on the ledger machine, still on 0.58.0\ncotal actor list > actors-before.txt\ngrep -rn 'actor grant' <your provisioning scripts>\n# make every grant name --scope, --allow-subscribe and --allow-publish, run them, then upgrade\nnpm i -g cotal-ai@0.59.0\ncotal actor list | diff actors-before.txt -\n```\n\nBoth refusals quoted here were run on 0.58.0 and on the 0.59.0 code. That brokers, managers and stored\nrows need nothing is read from the change, which touches only the CLI and its hints, and was not run\non a live split deployment.\n\n## Repeated flags refused in 0.59.0\n\nA `cotal` flag given more than once is now a usage error unless the command declares it repeatable.\nOn 0.58.0 the last value won with no message, so `cotal down web --space a --space b` acted on `b`\nwhile a wrapper that checked the first `--space` verified `a`. The break is in the command-line\nparser on the machine that runs the command, including commands added with `cotal ext add`. No\nstored state, credential or wire message changes.\n\n### What keeps working\n\nA command line that gives each flag once parses as it did on 0.58.0, in any order and in the\n`--flag=value` form. Flags whose help says repeatable, such as `--opt` and `down --session-store`,\nstill collect every value. A flag-shaped word after `--` is still a positional. The daemons, units\nand agents that `cotal` starts for itself are given each flag once, so a fleet driven only by `cotal`\ncommands typed by hand needs no action.\n\n### What stops working\n\nA command line that repeats any other flag exits 1 before the command runs. It prints\n`Option '--space' cannot be repeated`, or `Option '-f, --file' cannot be repeated` for a flag with a\nshort form, followed by the command's help. `-f` and `--file` count as the same flag. Look for it in\nscripts, aliases and wrappers that append a flag to override one set earlier, such as a fixed\n`--space` followed by `\"$@\"`.\n\n### Upgrade order\n\nChange those scripts first so each flag is given once. 0.58.0 and 0.59.0 both accept that form.\nBrokers, managers and participant machines need nothing for this break, and each machine's CLI\napplies it when that machine is upgraded, so their order is the one the sections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused command does nothing. The\nexposure is a script that still repeats a flag when it runs on 0.59.0: it exits 1 instead of acting on\nthe last value.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the scripts, aliases and wrappers\nthat call `cotal` so each one can be checked.\n\n### The upgrade end to end\n\n```sh\n# still on 0.58.0\ngrep -rn 'cotal ' <your scripts and wrappers>\n# give each non-repeatable flag once, then upgrade\nnpm i -g cotal-ai@0.59.0\n# run each changed script; a repeat left behind exits 1 with the usage error and does nothing\n```\n\nThe refusal and its messages were run against the 0.59.0 parser and `cotal topology view`. That the\nargument lists `cotal` builds for its own processes give each flag once is read from the code, and\nwas not run on a live split deployment.\n\n## Detached spawns from a seat's shell in 0.62.0\n\nOn a static or open mesh, `cotal spawn --detach` run inside a managed seat's shell now launches as\nthat seat. On 0.61.0 it minted a one-shot operator instrument, so the manager recorded that\ninstrument as the spawner and the seat's own `cotal_despawn` of the child was refused with\n`not authorized: <seat> was not spawned by <caller> (admin tier required)`. The break is in the CLI\non the machine where the seats run. No stored state, credential or wire message changes.\n\n### What keeps working\n\n`cotal spawn --detach` from an operator terminal or from a script outside any seat launches as\nbefore, and so does any call with `--creds`, one aimed at a space other than the seat's own, or a raw\nopen target named with `--server` and an unregistered `--space`. A user-auth mesh is unchanged. A seat with\n`capabilities: [spawn]` still spawns from its shell, and can now stop that child with\n`cotal_despawn`. `--on <instance>` from a seat's shell still lands on that manager instance, now as\nthe seat.\n\n### What stops working\n\n- On a static mesh, a seat without `capabilities: [spawn]` can no longer spawn from its shell. Its\n own credential holds no spawn subject, so the broker refuses the request and the command exits 1.\n- A child launched from a seat's shell is now that seat's child, so the manager stops it when the\n seat exits, as it does for a `cotal_spawn` child. A child that has to outlive the seat that\n started it now goes with the seat.\n- A seat launched without `COTAL_SPACE` is placed by its static credential. Every connector sets\n that variable, so this only reaches a hand-built launch: from such a seat's shell, a spawn aimed at\n a static space that holds no credential for the seat is refused instead of running as the operator.\n\n### Upgrade order\n\nOnly the CLI that seats run from their shell changes, which is the one installed on the host where\nthe seats run. Brokers and managers need nothing for this break, so their order is the one the\nsections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it. A child already running when you upgrade\nkeeps the spawner the manager recorded at its launch.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the agent files whose seats run\n`cotal spawn --detach` from their shell, note which of them lack `capabilities: [spawn]`, and note\nwhich of their children must outlive the seat.\n\n### The upgrade end to end\n\n```sh\n# still on 0.61.0: find the seats that spawn from their shell\ngrep -rln 'cotal spawn' .cotal/agents\n# add `capabilities: [spawn]` to each of those agent files that lacks it, and launch any child\n# that must outlive its seat from an operator terminal instead\nnpm i -g cotal-ai@0.62.0\n```\n\nThe attribution, the despawn, the refusal of a seat without `spawn`, the stop on seat exit and a\nseat's `--on` spawn were run on a local static mesh, and the attribution and the despawn on a local\nopen mesh.\n\n## From 0.53.0 to 0.54.0\n\nManager calls now borrow an instance-bound `manager-caller` credential. Followed mutations require\n`manager.goal-result` on the selected manager, so a compatible issuer, manager and client must be\nloaded together. An older manager is refused before a followed mutation; upgrading an installed\nbinary alone does not replace code in a running manager, connector or embedded client.\n\n### Preserve state before changing processes\n\nSnapshot the broker's durable storage using its supported backup procedure, the host authority and\nactor ledgers, and each participant's manager identity, runtime custody records, credentials and\nsaved sessions. Include the embedding application's database and configuration under its supported\nbackup procedure. Record the loaded package versions and the CLI path used by bearer helpers.\nKeep these copies private. Do not change the IdP issuer, regenerate manager identities, rotate agent\ncredentials or recreate tenant storage to make the upgrade pass.\n\nNo ledger, goal-history or session conversion is required for this change. Existing ordinary\nmessaging credentials retain their normal expiry rules. New manager-caller credentials are obtained\non demand from the current grant; old manager-call credentials do not gain the new view automatically.\nExisting accepted goals remain durable and must not be submitted again merely because observation\nwas interrupted. Fresh remote registration publishes its service status at the current revision and\nepoch; do not seed that status manually.\n\n### Upgrade the split deployment\n\n1. Stage one pinned 0.54.0 package set for the host and participants, including the embedding SDKs.\n Pause new manager mutations and let accepted work settle where possible before reloading processes.\n2. Upgrade the host issuer and embedding first. Keep the broker, its account identities and durable\n storage in place. Then load the matching manager release on each participating machine.\n3. Preserve active seats through the runtime's supported update path. A Linux custodial runtime may\n release and re-adopt seats within its 600-second unattended window; verify the actual runtime,\n custody records and process identities before relying on it. A legacy PTY runtime without release\n support cannot preserve active seats through a generic manager restart. Drain it at an approved\n idle window instead of signalling the manager or replacing conversations.\n4. Reload the clients and connectors through their session-preserving host controls. Refresh any\n bearer helper captured from an older immutable CLI path. A transport-only reconnect does not reload\n JavaScript. Verify authenticated instance selection, a read-only manager command and canonical\n result recovery before allowing new followed mutations.\n\nTreat the interval from issuer reload through compatible manager/client reload as a manager-control\noutage. Mixed versions can refuse discovery or commands; there is no promised rolling transition.\nOrdinary agent sessions survive only where their runtime and credentials permit it. If verification\nfails, keep mutations paused and repair forward from the preserved state rather than resetting it.\nThis release does not add host-backed enrollment or terminal release for stock participant detached\nagents; see [Remote supervised agents](run-a-mesh.md#remote-supervised-agents).\n\n## From 0.48.2 to 0.49.0\n\n0.49.0 changes how a credential's authority is recorded. A credential is no longer only a signed\nfile: it is an *issuance*, with a generation the issuer chose and durable evidence of the ceiling it\nwas granted under. The important consequence for a running deployment is not at connect time. It is\nat renewal time.\n\n### What keeps working without any action\n\n- **Existing agent credentials keep authenticating.** A credential minted under 0.48.2 is not\n revoked and is not rejected at connect. Nothing needs to be re-issued to bring the fleet back up\n after the upgrade.\n- **The channel registry survives.** Channels, their replay settings, descriptions, and usage text\n are ordinary durable state and are not rewritten by the upgrade.\n- **`cotal deliver` is still a standalone command.** Running the delivery daemon as its own process\n remains supported; it is not restricted to being a child of `cotal up`.\n- **`cotal join` keeps its flags.** In particular `--lifecycle-uid` is not new in 0.49.0. It has\n been required alongside `--creds` since well before this release, and the pairing rule did not\n change here. A scripted external join that worked under 0.48.2 works unchanged.\n\n### What does not migrate\n\n**A credential minted before 0.49.0 cannot be renewed.** Managed agent credentials carry a\n24-hour lifetime and the manager re-signs one once it passes **75%** of its life, ticking every\nquarter of the TTL so a tick always lands inside that window. When the manager reaches a credential\nthat carries no issuance, it refuses to renew it and logs the agent by name:\n\n```\n! managed cred renewal <agent>: renewManagedStaticCred: <agent> carries no issuance;\n a static credential minted before SPEC 13.15 is not renewed under an unbound generation\n - respawn the agent\n - the agent dies loud at this cred's expiry unless it is reminted\n```\n\nSo the fleet comes up fine, runs normally, and then each agent stops at its own credential's\nexpiry, within roughly a day of the upgrade, one at a time rather than together. The refusal is\ndeliberate: the renewal would otherwise have to invent a generation nobody issued, which is the\nstate the release exists to remove.\n\n**Respawn the managed agents as the last step of the upgrade.** For this particular upgrade the\nrespawn is not optional: stopping a 0.48.2 manager ends its agent processes whichever CLI you use,\nfor the reason given under the outage window below. The respawn is how they come back, and it is\nalso what mints each credential as an issuance so it renews from then on. One planned pass over the\nfleet is the whole job. Skipping it leaves agents stopped and, for any credential that survived\ninto 0.49.0 unminted, brings the renewal cliff above a day later, one agent at a time.\n\n### Credentials you minted yourself\n\n**A credential you minted with `cotal mint` is a different case, and it very likely needs\nnothing.** The distinction that matters is not the word \"static\", which covers both. It is **what\nminted the credential and who owns its renewal**. A credential the **manager** minted for an agent\nit spawned carries a lifetime and is renewed by the manager, so it is the subject of everything\nabove. A credential **you** minted with `cotal mint` and handed to an external peer is issued with\n**no expiry at all**, and no manager renews it: it is not in the sweep, so there is no renewal to\nfail. It keeps working after the upgrade, and re-minting it would mean coordinating with a third\nparty for no gain.\n\nThe manager says which one it is holding. Where a credential has no expiry to reach, the sweep\nnames it and moves on rather than refusing:\n\n```\n! managed cred renewal <agent>: credential is unbounded - not renewed\n (a pre-TTL credential stays as minted until respawn)\n```\n\nRe-mint an external peer's credential only if you want it to carry a lifetime, and at a time you\nchoose.\n\n### How to read the boot log\n\nA 0.49.0 manager starting over an existing space may print lines like:\n\n```\n verified evicted: <holder-key> (3/12)\n already verified (durable): <holder-key>\n\u2713 boot self-heal: manager/<id> registration gate reopened at generation <n>\n```\n\nThese are **not** a credential migration, and reading them as one is the most likely way to\nconclude the fleet is fine when it is not. They come from the manager repairing **one** endpoint\nregistration gate that a previous restart left frozen, and they enumerate that single gate's\ncredential-family holders as it verifies each one evicted. `already verified (durable)` on a later\nstart is the repair cursor resuming, not a credential that became durable. The repair is real and\nuseful (it is what previously needed `cotal reconcile-gate` by hand), but it says nothing about\nwhether your agent credentials carry issuances. The renewal refusal above is the signal that does.\n\n### Which side to upgrade first in a split topology\n\nMove the manager first.\n\nThe stores 0.49.0 introduces are created by the **manager** at its own boot, not by the broker.\nThey are create-or-verify and idempotent, so a 0.49.0 manager brings the space's authority stores\nup to the new shape itself, and it does so against whichever broker is answering.\n\nBeing honest about the evidence behind each direction, because they are not equally established:\n\n- **Broker-first was measured on a live 30-agent deployment** (issue #1578). Upgrading the broker\n first locks the old manager out immediately: `cotal up` re-renders the broker's generated config\n from the trust record, and after the restart the still-0.48.2 manager is refused on every\n connection with an `authentication error` naming the Nkey, continuously. That text comes from the\n broker process, not from a Cotal command, so match on its shape rather than on an exact string.\n `cotal ps` reports zero agents while\n the agent processes are still alive, because the manager has lost its view of them, not because\n they died. Upgrading the manager clears it immediately.\n- **Manager-first is reasoned from where the new stores are provisioned**, not from a measured\n fleet upgrade. It is the recommended order because the manager is the component that creates what\n 0.49.0 adds, but it has not been run end to end on a production split topology at the time of\n writing. Treat it as the better-supported order rather than a guaranteed one, and keep the\n rollback below ready either way.\n\nWhichever order you pick, **this is not a rolling upgrade**. Between the two steps the mesh is down\nand the manager cannot see its agents. Go straight through rather than pausing between them, and\nschedule it as an outage window.\n\n### What the window looks like\n\n- **The managed agent processes do not survive step 1, in either order.** This is the one place\n where the obvious reordering does not rescue you, so it is worth understanding rather than\n working around. Sparing agents on a bare manager stop is a **handshake**: a 0.49.0 manager\n publishes a capability file proving it can release its agents, and a 0.49.0 `cotal down` refuses\n the stop unless it finds one. **A 0.48.2 manager never publishes that file**, because the\n mechanism ships in the release you are installing. So the old CLI against the old manager sends a\n plain stop and takes every seat with it, and the new CLI against the old manager either refuses\n (leaving `--with-agents`, which reaps deliberately) or falls to the legacy path, warns that it\n cannot verify the manager can spare its agents, and signals it anyway.\n- **You can confirm which side you are on in one command, without stopping anything.** The flag that\n marks the newer behaviour is absent from the older CLI, and its summary line makes the difference\n plain:\n\n ```\n $ cotal down --help # on 0.48.2\n cotal down - stop the whole local stack, or name only the components to stop\n\n $ cotal down --help # on 0.49.0\n cotal down - stop the whole local stack (managed agents stay running unless --with-agents), ...\n ```\n\n If your `cotal down --help` does not mention `--with-agents`, stopping the manager stops the\n agents with it.\n- **Therefore the respawn in step 5 is mandatory recovery for this upgrade, not an optional pass.**\n It is also the step that re-mints credentials as issuances, so it is the same action either way.\n Plan the window to include it rather than treating it as cleanup.\n- The **manager's view** of them is lost while the two sides disagree, so `cotal ps` reports zero\n and control commands do not reach seats.\n- **Messages are not delivered** while the mesh is down.\n- The window is as long as it takes to restart the second component, plus the manager's own start.\n It is minutes, not hours, provided you do not stop between the steps.\n- **Nothing self-heals if you stop halfway.** The refusal is continuous until both sides match.\n\n### Snapshot this before you start\n\nTake these while the deployment is still on 0.48.2. The two `cotal` reads are live reads and must\nhappen before anything stops.\n\n- **A filesystem or volume snapshot of both containers**, if your platform offers one. This is the\n only rollback that covers every case, and it is what the reporting deployment used.\n- **`cotal backup create <dir>`**, for the durable space state, **but read the next paragraph before\n you rely on it**: on a split broker and manager topology it is very likely unavailable to you, and\n the volume snapshot above is your actual rollback.\n- **The trust records and credential directory** under `.cotal/auth` on the manager host, including\n the per-space material directory. These are what a re-mint would otherwise have to replace.\n- **A copy of the channel registry**, so you can verify it came back rather than assuming it did:\n `cotal channels list` before and after.\n- **The output of `cotal ps`**, so you know how many seats you expect to see afterwards and can tell\n a lost view from a lost agent.\n\n#### `cotal backup` on a split topology\n\n**`cotal backup create` cannot read a running stack.** It requires a completed cut, and only\n`cotal down --preserve-state` publishes one:\n\n```\n$ cotal backup create ./backup.0482\n\u2717 backup requires a completed cut; run `cotal down --preserve-state` first\n```\n\n**And `cotal down --preserve-state` requires a manager alive on the host you run it from.** It uses\nthat manager to attest that every retained child stopped, and the check is deliberately fail-closed:\na manager that is dead or merely uncertain refuses rather than preserving an unproven cut. The check\nreads a local pidfile, so a **remote** manager does not satisfy it. On a split topology the broker\nhost has no local manager, which means the documented durable-backup path is not available there.\n\n**Measured rather than assumed, at 0.48.2**: the backup refusal above is executed output. The\npreservation requirement is read from `down.ts` at the same tag, where the preserve path asks a\nmanager to prepare an inventory and then requires that manager to be locally alive before it\ncommits. The part not executed end to end is a genuine two-host split, which needs two real hosts.\n\n**What to do instead.** Use the filesystem or volume snapshot of both containers. That is the\nrollback the reporting deployment actually used, it covers the broker's durable state and the\nmanager's credential material together, and it does not depend on either component being able to\nattest for the other. If you want `cotal backup` as well, take it from a host that does have a live\nlocal manager, and understand it is a second copy rather than the primary rollback.\n\n**This looks like a product limitation rather than a documentation gap**, and it is written here as\none so an operator is not left thinking they mis-typed a command. The upgrade path for the exact\ntopology this page is addressed to cannot use the documented backup command.\n\n### The upgrade end to end\n\n```bash\n# 0. on 0.48.2, STILL RUNNING: record what you expect to see afterwards.\n# These two are live reads, so they must happen before anything stops.\ncotal channels list > channels.before\ncotal ps > ps.before\n\n# 1. manager host. READ THE NOTE BELOW THE BLOCK FIRST: this step ends the\n# managed agent processes whichever order you choose, and the respawn in\n# step 5 is how they come back. It is recovery, not tidying.\n#\n# STOP THE MANAGER WITH THE 0.48.2 CLI, BEFORE INSTALLING 0.49.0. The\n# order matters and it is not recoverable once you install: a 0.49.0\n# `down manager` REFUSES to stop a 0.48.2 manager whose pid record carries\n# a start token, which is every manager on a platform that can read one\n# (Linux can):\n# refusing bare manager stop: ... does not prove this manager can detach\n# its agents; use --with-agents or stop the agents explicitly\n# The refusal names two remedies and NEITHER clears it for this case. The\n# check reads a capability file that only a 0.49.0 manager writes; it never\n# counts agents, so stopping them first changes nothing. And `--with-agents`\n# is whole-stack only, so `down manager --with-agents` is refused by its own\n# flag rule. See #1592.\ncotal down manager # the 0.48.2 CLI, still installed.\n # 0.48.2 has no --with-agents; this\n # is the whole route. On a host that\n # runs the whole stack, the 0.49.0\n # `cotal down --with-agents` after\n # installing is the alternative.\nnpm install -g cotal-ai@0.49.0 # ONLY after the stop above\n# `supervise` RUNS IN THE FOREGROUND and holds the terminal until you stop\n# it. There is no --detach on this command. Start it under whatever keeps\n# your manager alive normally (systemd unit, container entrypoint, or a\n# second terminal), and run the remaining steps from another shell.\ncotal supervise --space <space> --server nats://<broker>:4222\n\n# 2. broker host: stop the stack.\n# NOT `--preserve-state` on a split topology: it needs a manager alive on\n# THIS host to attest its children stopped, and yours is on the other one.\n# Your rollback is the volume snapshot from \"Snapshot this before you\n# start\", not `cotal backup`.\n# See \"cotal backup on a split topology\" above.\ncotal down\n\n# 3. broker host: install 0.49.0 and start it again\nnpm install -g cotal-ai@0.49.0\n# Record the manager log's size BEFORE starting, so step 3a can tell THIS\n# boot's output from every earlier one. It must be captured here, ahead of\n# the start: taken afterwards it sits past the new line and the wait hangs.\n# `<spaceKey>` is NOT the space name. It is lowercase hex of the name's\n# UTF-8 bytes, so space `prod` is `manager.70726f64.log`. Do not guess it:\n# `cotal up` prints the real path on its launch line. Substituting the\n# plain name points at a file that does not exist, and the wait below then\n# burns its full timeout before telling you.\nLOG=.cotal/manager.<spaceKey>.log\nOFF=$( [ -f \"$LOG\" ] && wc -c < \"$LOG\" || echo 0 )\ncotal up --detach --host 0.0.0.0 --space <space> --no-manager\n\n# 3a. SPLIT TOPOLOGY ONLY: `--no-manager` above boots the broker (and the\n# delivery daemon) with NO local manager on the broker host, so there is\n# no wait-and-stop step on a current cotal-ai. The rest of this step is\n# the OLDER-host recipe, kept because the flag is refused there and that\n# refusal is your signal you are on it: without the flag the `up` also\n# starts a local manager, and you must wait for the log to show it is up,\n# then stop it, or you finish the upgrade with two managers and the one\n# you did not intend is the one nobody is watching.\n# A bare `grep -q` does NOT wait: it reads once and exits 1 immediately\n# if the line has not been written yet. Bound the wait instead, so a\n# manager that never comes up fails loudly rather than reading as ready.\n# The log is opened APPEND-ONLY, so on any host that has run a manager\n# before, this file ALREADY carries a `manager up` line from an earlier\n# boot. Grepping the whole file therefore matches instantly and waits for\n# nothing. Read only what THIS boot appended, using the $OFF captured in\n# step 3 above (before the start, which is the only point it is correct):\ntimeout 60 bash -c \\\n \"until tail -c +$((OFF+1)) \\\"$LOG\\\" | grep -q '. manager up'; do sleep 1; done\"\n# exit 0 = THIS boot logged it; exit 124 = it never did, so STOP and look.\n# This manager is 0.49.0 and publishes its own spare-capability file, so\n# the bare stop below is NOT the refusal case from step 1.\ncotal down manager # broker + delivery remain\n# On a current cotal-ai the two commands above are unnecessary (nothing\n# to wait for, nothing to stop) and `cotal down manager` simply reports\n# no manager to stop.\n\n# 4. verify the mesh is whole again before touching the fleet.\n# Do NOT compare `cotal ps` against ps.before yet: step 1 ended the agent\n# processes, so at this point it is EXPECTED to be empty, and an empty\n# `ps` is also the signature of the broker/manager mismatch described\n# above. The two are indistinguishable here, so compare what the mesh\n# itself should have carried across instead:\ncotal channels list # compare against channels.before: this SHOULD match now\ncotal ps # expect it to be EMPTY here; ps.before is the target for\n # step 5, not for this step\n\n# 5. the step that is easy to skip: respawn the managed agents so their\n# credentials are re-minted as issuances and can renew. Persona is a\n# POSITIONAL argument here, unlike `cotal stop`, which requires --name.\n# One call per agent:\ncotal spawn <persona> --detach --name <n> --space <space>\n# then the comparison step 4 could not make:\ncotal ps # NOW compare against ps.before: seat count should match\n```\n\nThe mesh is down from step 2 until step 3 finishes. That is the window. On a split topology there is\nno cut and no backup inside it, so the window is the stop, the install and the restart, nothing more.\n\n## Adding a section for a future release\n\n**Every changeset marked breaking adds a section to this page.** A release that changes what an\noperator must do, in what order, or what stops working, is not finished until the section exists.\n`scripts/upgrade-section-gate.mjs` grades a commit range for this: run it as\n`pnpm upgrade-section-gate --base <ref>` and it reds when the range carries a breaking change and\nadds no new release section. The former Attribution workflow enforced this on each pull request;\nthat workflow and its required-status rule were removed on 22 October 2026. The script remains\navailable for manual checks, and reviewers still check upgrade instructions for breaking changes.\nBe precise about what the check proves. It proves a section for a release\n**was written here**. It cannot prove the section is **correct**, or that it describes the break\nthat actually landed, and it cannot see a breaking change that carries no marker at all. Reviewing\nthe words remains a person's job.\n\n**Mark the break, or the gate cannot see it.** Any one of these is enough, and they are the only\nthings it reads:\n\n- a `!` before the colon in the commit subject, as in `feat(core)!: bind hosted runs to the caller`\n- a `BREAKING CHANGE:` footer in the commit body\n- a changeset in `.changeset/` declaring a `major` bump for any package\n\nThe marker must survive the squash. A `!` that lives only in a commit you squash away is not in the\nrange the gate grades, so put it in the subject that lands on `main`.\n\n**The heading is a `##` and names the release**, like `## From 0.48.2 to 0.49.0`. Both matter, and\nneither is a style preference. Coverage is claimed by a heading, so a heading that names\nno release claims every release and distinguishes none: `## Notes` with a sentence under it would\notherwise satisfy the rule. Naming the release also makes the section the one an operator upgrading\nthat release will search for. Use `###` freely for detail inside a section. Subsections belong to\ntheir release rather than counting as separate coverage.\n\nName the release that first carries the change: the next version Changesets publishes, which\n`pnpm changeset status --verbose` lists. `bin/package.json` on `main` still reads the release already\npublished. If a release is cut while the change is open, the change ships in the release after it,\nso move the heading before merging. The gate accepts any version in a heading, so before merging a\nrelease pull request, check every heading added since the previous tag against the version it\npublishes.\n\nA section is written for the operator, not for the reviewer. It answers, in this order:\n\n1. What keeps working with no action at all.\n2. What does **not** migrate, and when that becomes visible. Name the log line if there is one.\n3. The order to move components in for a split topology, and why that order.\n4. What the outage window looks like, including what survives it.\n5. What to snapshot before starting.\n6. The commands, end to end.\n\n**Where an answer was not measured, say so in the document rather than guessing.** An operator who\nknows which half of a recommendation is reasoned and which is measured can plan around it; one who\nfinds out afterwards cannot.\n\n## Handle redemption in 0.91.0\n\n`auth redeem <private-handle-file>` selects the native auth target, then refuses unsupported\nconferring-handle redemption without reading the file or issuing credentials. It does not redirect\nthe artifact into agent enrollment or interactive-session redemption. Offline handle inspection\nstill labels signatures, revocation and holder currency not checked. An explicit `--space`\nrequests static-registry signature inspection with a native ephemeral operator reader. It checks\nsignatures, issuer scope, presented links and required revocation records without establishing\npresenter identity, live epochs or use authority. It confers no holder credential, persists no\nreader credential, and never turns missing or stale native records into verified offline success.\nOpen/user-auth inspection and conferring redemption remain unsupported.\n\n## Dashboard service transition in 0.91.0\n\nThe matching dashboard package now registers a service preset and hidden native child, not a\npublic web command. An older cached command descriptor is hidden and refuses with the matching\nextension-update remedy before importing provider code or repairing the install. `update` remains\nexplicit; `service start web` never installs a deliberately absent extension.\n\nConfigure the saved web definition with its existing registered static-auth root, one protected\ncredential file and explicit read-only intent. The service retains required TLS, creates no caller\nor channel-purger credential, opens no browser and prints no bootstrap secret into logs. The page\nmarks it read-only and hides delete; the server independently rejects that mutation before any\nalternate connection. The private session file holds the full-launch bootstrap; logs give its\nreference only. User-auth/open targets and OS installation remain unsupported in this route.\nExisting legacy process records are still observed, never silently adopted. An already-running\nlegacy dashboard is not restarted or converted by loading metadata. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\n## Registered lifecycle retirement in 0.91.0\n\n`auth retire <full-lifecycle-uid> [--yes]` has closed UID/space input, but the standalone CLI still\nlacks a supported native retirement instrument carrying its own current serve identity and gate.\nIt refuses without publishing a request. It does not borrow the manager's identity, treat --yes as\na grant, or call the host's interactive/managed loopback doors as a substitute. This remains an\nexplicit authority gate, not a completed retirement operation.\n\n## Local-process `rootedAt` removal in 0.91.0\n\nA local-process extension descriptor no longer has a `rootedAt` field. No stop has read it since\nthe dashboard became a service. Operators have nothing to migrate, snapshot or order, and there is\nno outage: a manifest that an older install cached with the field still loads, and the next\nmanifest rewrite drops it. An extension author removes the field from each `LocalProcess`\ndeclaration, where TypeScript now reports it as an unknown property.\n"
|
|
27288
27288
|
},
|
|
27289
27289
|
{
|
|
27290
27290
|
"slug": "watch-a-mesh",
|