# client

## Responses cybersecurity access

`client/catalog.CatalogModel.cyber_access` holds an optional typed
`provider/model.CyberAccessProgram` selected by `[models.<name>].cyber_access`.
Only `openai-responses` and `codex-subscription` accept the key. The catalogue
attaches it through `provider/gateway.with_cyber_access`; the opaque gateway
indexes selections by provider name, and each actual attempt passes that
entry's option into the Responses request encoder. Missing options omit
`access_programs`; explicit options send the bounded `cyber` value. Provider
approval remains remote. A 403 denial is terminal, and retryable fallback uses
its own entry's selection. The frozen resolved identity and request types keep
their existing shape.

## Per-strand shell directories

`client/working_directory` stores canonical shell defaults under reserved
`client/working_directory/<strand>` facts, through `owner_services.FactAccess`
(whose `cell` and `put` use the projected fact supplier from
`agency.fact_supplier`). Missing state means workspace; corruption,
unavailable storage, a deleted directory or a redirected canonical target is
an explicit error. An absolute setter path allows recovery. Writes compare the
previous sequence. `serve` installs the native tool and wraps the existing
code-mode router so `proc.run` shares the default and `proc.in_dir` overrides
one call. File/search/LSP paths remain workspace-relative. `jobs.Request.cwd`
passes through `jobseam.Door.start` into durable `JobSpec.cwd` before launch.

Finite LSP observation inference selects the first source's configured owner
and checks every remaining source against it before acquisition or text reads.
Explicit assertions remain exact, and mismatch errors name both owner pairs.
Installed Gleam inventories compare parsed contents with recursively ordered
table keys. A serializer-only reorder cannot evict a warm server; versions,
Git commits and every supported string value remain part of the stamp.
Unsupported values and excess table depth refuse reuse.
Project configurations and manifests retain their byte hashes.

## LSP query handle ownership

`lsp/manager.Manager` retains reachability, workspace/server identity,
search, protected paths, timing and display roots. Its caller-side handle
excludes `Backend.connect`; the manager actor and keeper retain the full
configuration and perform transport startup. Every query and stop still
resolves the same restartable address and uses the original bounds. The
capture regression grows only connect's environment and requires both the
handle and its eight door callbacks to remain the same flat size. The returned
server transport projects the abort callback and step identity before retaining
its abort closure; environment lookup stays with preparation, tested by growing
only its captured payload through the scripted enforcement probe.

## Context observation and router capture

`context_view.reader` projects the immutable tool registry into a private
name-keyed table containing only names, descriptions and schemas before
constructing the hub's session-lived reader. Every observation still captures
the current strand configuration and leaf together, then selects active tool
names from that configuration. Sorting, deduplication, missing-name omission
and duplicate registration replacement retain their existing semantics.
`serve` also projects the fallback context window and compaction settings
before retaining the reader. The public `read` and `inventory` adapters share
the projected implementation; the production reader projects once at boot.

`serve.with_code_mode_peers` retains the preceding `wrap_router` function
before composing the peer router. It calls that wrapper first and binds peer
access to the launching request's strand. The returned host still owns its
execution configuration, but the router callback no longer duplicates the
unrelated configuration in its environment. Capture-size regressions grow
unrelated executor or host payloads and require both readers to remain the
same size. Behavioral assertions independently pin observation and routing.

`codemode.serving` also fills `Config.strand_tools` from the Agency's `holds`
for every seam selection, and the execution configuration turns it into
`tool_gate.precheck` on the satellite host. The precheck runs in each call's
worker and refuses a capability whose authorizing tool (`codemode/tool_gate`'s
table: `bash`, `fs_write`, `fs_edit`, `job_*`, `schedule_*`, `agent_*`) the
calling strand's durable active list lacks. `None` means no messaging plane
and no check. The extension dispatch host is a separate configuration and
never installs it. `agency.holds` returns `ToolNotHeld` only for a tool
absent from a readable list and lets `AgencyUnavailable` and `PlaneFailed`
flow through.

`async_codemode.launch` likewise projects the preceding router before
constructing the execution's wrapper. Its admitted work closure retains the
prepared configuration once; the wrapper adds no path to the original host
configuration. The regression observes the actual `async_runs.Launch` message
after a process copy, grows an unrelated entropy payload, and requires exactly
one payload's growth while preserving the execution identity and deadline.

## Gateway membership and restart captures

The gateway projects `Options.registry` into a private name-keyed dictionary
before building its actor state. That state distinguishes an absent registry
from a configured empty registry, lists sorted registered names, and validates
operator selections and history-tool availability without keeping tool
executors in this slot. Public startup options still accept the same registry
used by the runtime effect surface; execution remains owned by that surface.

The async-run and hub child-start callbacks project their heartbeat, session,
workspace, catalogue and code-mode refusal inputs before constructing the
restart specification. Those callbacks retain the runtime inputs they require,
but no longer add a path through the complete startup `Settings` record.

## Transfer captures and a slow storage actor

`begin_transfer` does not wait for storage. It starts a one-task weft run
whose task calls `snapshot_reader.capture`, so the run owns the reply subject
and the five-second deadline, and the answer returns to the hub as
`CaptureReported` through the pending entry in `State.captures`. The entry
holds the request's reply capability, because `request_frame` clears the
connection's own copy when it returns. A late reply goes to a task that has
already exited, so it is never an unexpected message in the hub's mailbox, and
the hub keeps serving every other attachment while one capture is outstanding.

`reader_failed` fences the hub (`ReaderPoisoned`, every attachment closed, the
incarnation's stop capability invoked) only for `snapshot.ReaderUnavailable`,
which `storage/internal/snapshot_call` reports when its monitor sees the
storage actor die. `ReadTimedOut` is only a deadline. It refuses the one
request in band as `snapshot_failed` ("retry the request"), drops that
transfer, and leaves the attachment open for the client's retry. The cost is
that a storage actor which is alive but never answers no longer retires its
session by itself; each retry queues one more capture behind it, bounded by
the one request a connection may have in flight. The goal, advisor, block
summary and notes reads still call `capture` from the hub's own process, so
they can still receive a late reply there; they take the same refusal.

## Provider observation capture

Admission checks the selected model's image-reading capability before asking
for image classification. Image-capable identities, including the existing
unknown-model fallback, need no operation or transcript reads for this routing
decision. Text-only identities retain the durable projection and admitted-batch
checks which keep image-bearing continuations on the vision route.

`wiring.request_image_classifier` projects the session before constructing
the callback used by `serve.summary_tap`. The dispatcher and the block summary
observer still classify the current context and immutable admitted prompt
batch with one implementation. The observer carries no tool registry or
executable tool callbacks. Its flat copy cost must stay unchanged when an
unrelated registration's run payload grows; `wiring_test` pins that property,
and `vision_test` covers held image batches and their continuations through
the classifier.

`wiring.build_effects` separately projects `ProviderRouting` and
`ProviderConfiguration` before retaining provider callbacks. The latter carries
the session, pinned system prompt and a name-keyed table of `model.ToolSpec`;
tool executors, requirements and environment remain in `Effects.tools.run`.
The table comes from the same immutable registry used for execution. Each
request's durable active names still control selection, sorting, deduplication
and omission of unknown names, so prompt cache prefixes and authorization
remain unchanged. Public request and target helpers retain their signatures and
use the same projected implementations as the production surface.

`Effects` is copied into every process and supervisor child specification
that holds a session's runtime, and a closure over `wiring.Config` puts a full
copy of the tool registry into each one. `client/serve` therefore builds its
effects with `wiring.build_effects_held`: the `run` slot captures only the
address of a `client/tool_holder` process, which keeps the one `Config` and
hands it back to each tool run (`run_tool_held`). A holder that is gone or does
not answer inside five seconds yields an in-band `ToolCompleted` failure, never
a crash. The holder is published to custody as `instance_owner.ToolConfig`
before `api.open_published` and retires directly after `Runtime`, because
tools run until the runtime drains. `build_effects(config)` keeps the capturing
closure for tests, the scripted demo and extension hooks.

## Code-mode alternatives on direct tools

`contributions.built_in` appends a concrete capability call and result shape to
`fs_read`, `fs_write`, `fs_edit`, `grep` and `bash` descriptions when code mode
is wired. Each hint requires both the module in the default seam's import
allowlist and the call in its serviced capabilities. An alternate seam alone
cannot justify a call that omits the seam argument. The tool-index snippets
retain their direct contracts; the descriptions carry the alternatives once.
Text reads omit image blocks, windows and anchors; capability edits use unique
string replacements; `proc.run` is foreground argv execution without shell
expansion. The native tools' schemas and execution remain unchanged.

## Operator startup diagnostics

Protocol 055 records the bounded operator diagnostics contract. The manager's
`StartFailed(reason)` retains one line of at most 2048 UTF-8 bytes in the
existing capacity-capped operation memo. Both session builder errors and
failed domain construction memo the initiating session operation before
cleanup; `daemon/server.dispatch` sends that reason only after epoch and
session authority checks for `operations.get`. Exact failures precede live
`Stopping` status while retirement holds the reservation; next admission
clears the old memo before builder publication. The log retains the domain
failure class and adds the same bounded configuration reason. Retirement
still owns replacement permission; a diagnostic is never custody evidence.

`serve.extension_registrations` returns the successful registrations and
bounded refusal notices from the same discovery. `gateway.with_extension_refusals`
captures at most 32 notices of 2048 bytes in `Options` and actor state;
`captured_transfer` includes them in `tool_availability.extension_refusals`
for every authenticated snapshot. Assembly reports up to 32 refusals, or the first 31 and an
explicit `loom ext list` overflow notice. Reinstall guidance follows the
existing remove-then-install policy. Refused extensions register no tools.

`hookrunner.settled` preserves broker failure text in bounded stderr while
keeping exit 1 and empty stdout. A degraded execution includes the exact
enforcement entries; a timed-out or cancelled degraded execution instead
returns `WallCancelled` with both streams empty. `hook_diagnostic_test`
proves these outcomes through the real broker and helper wire protocol,
independent of host enforcement availability.
This proof ends at `hookrunner.Outcome`: existing compatibility consumers
may discard exit-1 stderr. Transcript or debug-log delivery through those
consumers is not established, and their decision policy is unchanged.


## Remembered permission decisions

`permissions` owns `fact.custom/client/permission_grants`, separate from the
existing-directory additions. It accepts canonical readable/writable paths,
including exact files that do not exist yet, and full network access. Every
other grant type remains once-only, except exact-action session wall consent
described below. `gateway` validates the displayed action,
sequence and echoed subset before preparing a union of those echoed grants.
The runtime atomically commits that fact and approval under both expectations.

`wiring.run_tool` is `read_authority` followed by `run_workspace_tool`.
`read_authority` reads the stored directory additions, standing grants and
exact-action consent from the session store and never touches the filesystem
(`directories.read_stored`, `permissions.read_for_stored`); the result is the
plain-data `wiring.Authority`. `run_workspace_tool` revalidates it against the
local filesystem (`directories.revalidate`, `permissions.revalidate`), then
widens the policy and dispatches, taking only a `WorkspaceView` of the `Config`
plus the two owner functions (`refused`, `output`). The fused `directories.read`
and `permissions.read_for` remain as compositions for other callers. The
split is what lets a workspace on another node run the second half. It
validates and snapshots remembered grants once. Their path
component joins explicit native access; their path and network component joins
the jail policy. Protected writes remain denied. Reopening the session restores
this authority, while a paused call resumes with its consumed call approval and
already-running jobs keep their earlier snapshot. A malformed fact or retargeted
canonical path refuses dispatch. See protocol 041.


## Session directory authority

Production retains a projected `api.FactHandle` in the directory supplier
before constructing the hub's restart callback. `directories.admin_with_facts`
owns only that supplier, workspace and protected paths for mutation; its
readback callback owns Storage alone. The gateway still owns Runtime for
execution. The existing `admin` constructor acquires its Runtime lazily after
filesystem validation and projects the handle when called, preserving its
unavailable behavior. Conditional commitment and authenticated origin follow
the same path for both constructors.

`directories` owns the reserved `fact.custom/client/directory_access` record.
The operator gateway accepts `set_config.add_directory`, validates the real
existing directory and enforces protected paths for write additions, and commits through
`api.put_reserved_fact_expecting`. The recorded origin attributes the change.
Observers cannot mutate it, and model fact writes cannot name the reserved
key. Directory additions survive reopening the session; unreadable, malformed
or retargeted state refuses tool execution.

`wiring.run_tool` captures additions once and derives the jail policy and
native `directory_access.Access` separately. Jobs receive the invocation's
captured policy through `jobseam.Door.start` and `jobs.Request`; they do not
change when later tools receive new access. Code-mode filesystem and search
closures capture explicit native roots plus consumed call grants. Installed
extensions retain their separate installation authority.


## Streamed response handoff

Provider observation identities include the reserved response entry for
generation and poll requests. The runtime supplies that ID from its durable
intent; `gateway.request_identity` never mints a replacement. This lets the
terminal distinguish provider completion from the delivery of its saved answer.
See `protocol-change/036-stream-response-handoff.md`.

## Web view (`loomd --ui`)

`protocol-change/051`, its operator addendum, and ADR-014. `daemon/main`
parses `--ui` into `Config.view` (`ViewOff | ViewOn`). With it, `run`
starts `daemon/ui_sessions` and passes `server.Ui(sessions, upgrade)` to
`listen_serving`; without it `server.Config.ui` is `None`, every `/ui` path
is a 404, the control `hello` has no `ui` field and `ui.link` answers
`unavailable`.

- `daemon/ui_project`: derives each workspace's project for the web view's
  lists, with no cache and no process. `locate(workspace)` reads
  `<workspace>/.git` through one `file_info` (which follows symlinks): a
  directory is a plain checkout (the workspace is its own project); a file of at
  most 4 KiB is a worktree's `gitdir: <repo>/.git/worktrees/<name>` pointer,
  followed to the main repository (a bare repository's common directory is its
  own project) and accepted only when git's backlink
  `<repo>/.git/worktrees/<name>/gitdir` (same size cap) names this workspace's
  `.git`, so a hostile `.git` cannot claim someone else's repository. Anything
  else is `None`. Plain Gleam and `simplifile`, no FFI. `ui_socket.with_projects`
  maps it over the entries: inside the session page's list task
  (`listed_task`), and, for now, beside `home_listing` in the home's `sessions`
  closure, which moves into the home's read task with it. The field is in
  process, not on a wire, so no protocol addendum.
- `daemon/ui_sessions`: one `weft/actor` owning the ticket table (60 s,
  single use) and the UI-session table (8 h), both keyed by the SHA-256 of
  the secret. A `Grant` carries a `Scope` (`Session(id)`, `Home` or `Admin`,
  protocol-change/065), the minting credential's digest, the principal, the
  page's `ceiling` (`Observer` unless `ui.link` named `page:"operator"`), a
  `Reach` (`OneSession | Workspace`), an `Origin` (`Fresh | Resumed`) and a
  `Remember` (`Remembered | Forgotten`) (protocol-change/065, PR 8). `ui.link` without
  `session_id` mints a `Home` grant; the home routes (`ui_http.HomeExchange`,
  `HomePage`, `HomeSocket`, `server.home_grant`/`home_socket`) and
  `ui_socket.upgrade_home` serve it, a home living `ui_sessions.session_ms`.
  The router hands each page's `grant.reach` to `ui_socket.upgrade` and
  `upgrade_home`, which build the page's `Standing`, and every ticket a page
  mints carries it. The admin page's three routes (`server.admin_grant`,
  `admin_socket`, `Ui.admin`, `AdminAttachment`) check the cookie's grant is of
  the `Admin` scope and its credential authenticates as the owner at every
  request, and `ui_socket.upgrade_admin` serves it.
  Redemption is one message: it spends
  the ticket, answers `UnknownTicket` or `OtherScope`, and mints three
  secrets, ending no other page except the principal's oldest when it
  already holds `max_pages` (four, `ending.max_pages`) live pages for the
  session (each `Page` carries a serial for the order, since the clock can
  tie; an expired page is not counted), the cookie, the page key and the nonce, keeping only their
  digests in an opaque `Page`. `keyed` and `admits` compare a presented
  key or nonce by digest in constant time. `still_open` is the check an
  open page runs with every frame. `actor.periodic` sweeps; every read
  checks the deadline itself. It also holds each page's reader of its images
  (`register_images`, `images`; protocol-change/051, the addendum on images):
  the page socket registers, under the page's cookie, a function that asks its
  own component, and the image route reads it back. It is found only through a
  live UI session, is replaced by a reload's new socket, and is dropped by the
  sweep with the page.
  The browser login (protocol-change/065, PR 8) rides on the same table. A
  `Remembered` ticket (`loom ui`'s home, a device link) sets a login when it is
  exchanged: `server.entered` is the one function that builds a redeemed ticket's
  response, and it asks `ui_login.issue` for a token and a catalogue row, sets the
  `loom_login` cookie and the login's key and nonce in the body, and attaches the
  login to the page (`attach_login`). Every ticket carries the `Issuer` (the
  login's fingerprint, expiry and key) of the context that minted it
  (`mint_in`, `mint_device`, `mint_resumed`), so a page knows its login
  (`login_of`), a device link inherits the issuing login's expiry, and a chain of
  switches neither loses the login nor becomes fresh: `Origin` is carried onto
  every ticket a page mints. A device link's ticket lives `device_ms` (ten
  minutes). `Exchange` (`SessionExchange(id) | HomeExchange | AdminExchange`) names which
  exchange a ticket is redeemed at. `ui_socket.fresh_home(reach, origin)` is the
  one rule for who may open the admin page and refuses a `Resumed` home; the
  admin page's sign-in reads and `RevokeSignin` run beside its other changes
  (`admin_logins`, `revoke_signin_for_admin`), and its ticket carries the
  pressing page's login so it marks "This browser".

  An `Admin` page (the fifth addendum) lives `admin_ms` (fifteen minutes, never
  more than `Settings.session_ms`) and is counted against `max_pages` apart from
  homes and sessions; its ticket redeems only at the admin exchange.
  The same actor keeps the page-minted
  invitations' allowance, which the admin page's invitations, rotations and raised
  roles share with the session page's invitation control: `reserve_invite` and `release_invite`, keyed by the
  credential's fingerprint and not by any page, at most `invite_limit` (three)
  in any `invite_window_ms` (an hour, the claim's own lifetime), counted and
  taken in one message. A refusal answers the wall-clock Unix time at which a place
  frees (`Error(free_at_ms)`, the `invite_limit`-th newest reservation's instant
  plus the window, carried from the monotonic clock to the wall clock), which the
  admin page words as the count and the UTC time (`grants.TooMany`).
- `daemon/ui_login`: the daemon's half of the browser login (protocol-change/065,
  PR 8). `root_key(state_root, registry)` reads `browser.key` through
  `host/login.probe_root` or, when it is missing, revokes every login row
  (`manager.revoke_all_logins`), logs `daemon.logins_revoked` and only then writes
  a new key (`write_root`); a key that is present and wrong refuses `--ui`.
  `issue(root, registry, grant, parent, now)` draws the login's identifier, key
  and nonce, signs the token, writes the row (`manager.issue_login`, whose digest
  is `access.browser_digest(login.row_digest(id))`) and answers `Minted`;
  `parent`'s expiry becomes the new login's, and `issued_by` its fingerprint.
  `resume(root, registry, cookies, key, nonce, now)` opens each cookie value in
  turn (`login.open`: parse, chain, caveats, key, nonce, clock) and only then
  asks the registry (`manager.resume_login`: the row is active, of kind
  `Browser`, and the token's principal). It logs `daemon.login_issued`,
  `login_resumed` and `login_revoked` with the fingerprint and never a token.
  `server.login_resume` is the `POST /ui/l/<key>/home` handler: sender
  (`ui_http.same_origin_post`), declared form (`form_declared`), a `Control`
  parser permit, the body's `nonce`, `ui_login.resume`, then a ticket minted and
  redeemed in the same request (`Resumed`, `Forgotten`); a login narrowed to a
  session mints a `Session` page of that session (`OneSession`) and never a home.
  `GET /ui/l/<key>/home` is `page.login_page`, served under `form-action 'self'`.
  `claim(root, registry, claim_digest, name, now)` is the browser claim's side
  (PR 9): it draws a login, binds its row to the claim with `manager.claim_login`
  (an `Operator` login ending thirty days out, the name in the same transaction),
  and only then signs the token for the principal the catalogue names. The route
  is `server.claim_submit`: `same_origin_post`, `form_declared`, the body
  (`ui_http.posted_claim`), the token's shape (`host/claim.validate_token` on the
  trimmed value, before any lookup or permit), `root.acquire_claim` (one
  reservation per claim, as `/v2/claim`; a second post in flight is 409),
  `ui_login.claim`, then a `Home`
  ticket (`Fresh`, `Operator`, `Workspace`, `Forgotten`) minted and redeemed in
  the request, the login attached to the page, and `server.enter_response` (the
  half of `entered` that writes the cookies and body) sets it. A refusal is the
  claim form again under `form-action 'self'` with one fixed paragraph
  (`page.ClaimNotice`); the success is under `form-action 'none'`.
  `ui_sessions.Settings.wall` is the wall clock a ticket's login expiry is
  compared with, since `now` is monotonic: a `Remembered` ticket whose login has
  ended is `UnknownTicket` before it makes room; a `Forgotten` one is not held to
  it.
- `daemon/ui_http`: pure checks. `route` also routes the admin page's three
  (`AdminExchange` at `/ui/admin?ticket=`, `AdminPage`, `AdminSocket`) and the
  claim's two (`ClaimPage`, `ClaimSubmit` at `/ui/claim`); `posted_claim` reads
  the form's `token` and optional `name`, each once and no other field. `route` (the exchange at
  `/ui/sessions/<id>?ticket=`, the page at `/ui/p/<key>/sessions/<id>`,
  its socket at `.../ws` with the `csrf-token` query, and five assets),
  `loopback_host`, `exchange_allowed` and `navigation_allowed`
  (`Sec-Fetch-Site` is `none` or `same-origin`), `origin_matches`, the
  `loom_ui` cookie (`HttpOnly`, `SameSite=Strict`, `Path=/ui/p/<key>`) and
  `secured`, whose `Referrer-Policy: no-referrer` is load-bearing. `route`
  also routes `Image(key, id, ref, position)` for
  `/ui/p/<key>/sessions/<id>/image/<ref>/<position>` (a name of digits, `.`,
  `~` and `-` up to 48 characters, and a position below `max_position`;
  anything else is `Unknown`).
- `daemon/ui_assets`: the page's stylesheet, bootstrap scripts and client
  components' bundle (from `web_view`'s `priv/static`, `page.static_file`,
  built from `web_client` by `make gen-client`) and Lustre's client runtime
  (from `lustre`'s `priv`), read once by `main` when `--ui` is on and held
  in `server.Ui`. A release that lost one refuses `--ui` at startup; a
  request never touches the disk.
- `daemon/server`: routes in 051's order of checks. The exchange answers
  200 with the keyed path and the nonce in the body, never a redirect.
  `page_grant` re-checks the cookie, its key, its session, and
  `manager.authenticate` and `session_authority` for the minting
  credential on every request. The socket also requires the nonce, then
  goes through `resident_upgrade` with `PageRole(ceiling)`, which caps the
  attachment's authority with `ui_relay.capped` and admits it with that
  role's parser permit. `UiLink` mints a ticket for a member of the
  session. The image route (`web_document`'s `Image` arm) requires a fetch
  site of `same-origin` or `none`, then `page_grant`, then the page's registered
  reader, and answers what `web_view/image.serve` allows: 200 with the checked
  type and the view's headers, 404 for an image the page did not draw or a page
  with no reader, 415 for a type or bytes that are not a raster image and 413 for
  one over 20 MiB. `Ui.upgrade` takes the registration function as its fourth
  argument, ahead of the ceiling.
- `daemon/upgrade_log`: the record of an upgrade the daemon itself slowed
  or refused. `server` times each root and registry question on the way to
  an upgrade (`ready`, `authenticate`, `session_authority`, `get`,
  `resolve_incarnation`, `acquire`) and writes `daemon.upgrade_slow` for a
  step of 250 ms or more and `daemon.upgrade_refused` when the daemon was
  the one that could not answer, including a session that is not resident
  (the 409). A socket closed before its first frame writes
  `daemon.socket_closed_early`. Lines carry a route class, a step and a
  fixed reason, never a credential, key, nonce or ticket.
- `principals.rename` (protocol-change/065, the tenth addendum): `protocol.RenamePrincipal`
  decodes an optional principal, an unjudged name of at most 1024 bytes and the
  epoch; `server.dispatch` answers it through `manager.rename_principal`, one
  registry turn that authenticates the caller and epoch, lets a member name only itself
  (the owner any principal), applies `storage/access.rename`'s claim-time name rule and
  drops the authority memo. A refused name is `invalid_name`. `ui_socket.rename_self_for`
  and `rename_for_admin` are the home's and the admin page's daemon checks;
  `home_rename_self_capability` hands the home its capability, and `home_event` admits a
  `submit` beneath `home.signins_path`.
- `daemon/ui_socket`: the page's mist socket. It transfers the permit in
  its first handler turn and starts `web_view/component` for an observer's
  attachment or `web_view/operator_page` for an operator's, with a
  transport over `ui_relay` whose `connect` returns at once, and with the
  session's catalogue name and workspace for the page's heading, from the
  registration the route read when it resolved the session
  (`server.Attachment.registration`), and a digest of that workspace path
  (`component.Start.workspace_digest`, the lower-case SHA-256 in hex) that
  the browser keys the reader's saved layout by, so a path is never an
  attribute or a storage key. An observer's
  socket forwards only the "Load older" click at `component.older_path`, the
  context breakdown's Refresh click at `component.context_refresh_path`
  (protocol-change/075), the chips, the Home button and a fold's divider
  and drops every other browser message (`observer_accepts`,
  protocol-change/051, the addendum on history paging); an operator's
  forwards only the
  click and submit events its page attaches (`operator_accepts`) and takes
  frames up to `operator_frame_limit` (12 MiB, which holds a draft and 8 MiB of
  images at their base64 size; an operator's page is admitted as
  `root.PageOperator`, charged `root.operator_peak`, 64 MiB, for a submit's five-copy peak; an observer's stays 64 KiB). It registers the page's image reader with
  `register` when its component starts (`Page.images`: a `lustre.dispatch` of
  `ImageRequested`, answered on the asking handler's subject within two
  seconds, and refused at once when the socket's process is gone). It closes on the relay's
  `Ended`, with the close code `web_view/ending.close` gives that reason (1000
  after a quarter second for an ending the person resolves, 4000, through
  `mist.stop_abnormal`, for one that may clear), and shuts the component down
  on close. The two failures in `admit` that are the daemon's alone (the
  permit's transfer, the component's start) also close with 4000, which
  Lustre's client runtime retries; a 1000 there left the tab empty for good.
  `upgrade` takes the ticket table's handle (`ui_sessions.Sessions`, which
  `daemon/main` holds), so an operator page can open another session
  (protocol-change/051, the addendum on switching sessions): the transport's
  `open` is `opened_for(role, ..)`, which declines an observer's page without
  asking, and otherwise `ticket_for(standing, tickets, open, target)` (a
  `Standing` is the registry, credential digest, principal, ceiling and reach of
  the asking page, from `page_standing` or, for a home, `home_standing`),
  which checks with the page's credential digest that the identity is a
  canonical session's (after `open()` says the asking page is still open, which
  also yields its deadline, carried onto the ticket by
  `ui_sessions.mint_before`, so a chain of switches never outlives the page it
  began from; `ui.link` tickets keep eight hours), that `manager.session_authority` finds the principal's
  membership in it and that `manager.get` reports it resident, then mints a
  ticket into the same table with the page's own principal, ceiling and reach
  (`sessions.Ticketed(path)`, else `Declined(NotHeld | NotRunning |
  Unavailable)`). `observer_accepts` still drops a click beneath
  `component.sidebar_path` and now also admits one at exactly
  `component.home_path`; `operator_accepts` admits any click, as before. A page
  of `Workspace` reach is handed `Transport.home` (`home_capability`), whose
  `home_ticket_for` checks the page is open and the credential still
  authenticates as its principal, then mints a `Home` ticket (`Workspace`
  reach, the page's deadline and ceiling, never remembered, `Declined(NoHome)`
  on any refusal). The home's socket takes a click beneath `home.table_path` or
  `home.sidebar_path` (`home_accepts`) and its `Start.open` is `ticket_for` with
  the home's `Standing`. A home also manages its principal's browser logins
  (protocol-change/065, PR 8): `signins_read`, `sign_out_for` and
  `sign_out_all_for` read and end the principal's own logins through
  `manager.signins`, `revoke_login` and `revoke_logins` (the registry applies the
  own-or-owner rule, drops its frame memo and answers the digest the page logs),
  and `device_link_for` mints the ten-minute `Remembered` device ticket for a
  `Fresh` home only, after `reserve_invite`; `device_capability` hands the control
  to a fresh home alone and the daemon refuses from a resumed one regardless.
  `home_accepts` also takes a click beneath `home.signins_path`. `Standing` carries
  the page's `origin` and `login`, from `server.PageGrant`, which the router builds
  from the grant and hands to both `upgrade` and `upgrade_home`.
  A saved session opens through `resume_for(standing, tickets, open, target,
  within:)` (protocol-change/065, the third addendum): the page open, ceiling
  Operator, a canonical identity, `session_authority` Owner or Operator
  (`NotOperator` for an observer member), `manager.open` (any refusal is
  `NotOpened`), a `weft/poll` over `manager.get` until `Resident` for at most
  `resume_wait_ms` (30 s), then `ticket_for`. `resume_task` runs it in a weft
  run of its own (one task, no deadline, `start_witnessed`, linked
  to the Lustre runtime that called) and returns at once; the task's last act
  is `deliver(answer)`, which the component dispatches as `Linked`. `resumed_for`
  is the socket's gate: an observer role is refused `NotHeld` before any task.
  `listed_entry` maps `Reserved` and `RecoveryBlocked` to `sessions.Blocked`.
  The home and the sidebar list through `held_entries`, which reads the page's
  custody once (`manager.custodies`) and applies `with_moves`: a session in a
  move becomes `sessions.Moving(to)` and a tombstone is left out
  (protocol-change/085); a failed custody read leaves the rows as the catalogue
  says them. The admin drawer lists through `admin_rows`, the same `held_entries`
  plus the views it draws, so it reads no summary for a tombstone.
  The owner's home creates a session (protocol-change/065, the fourth addendum).
  `home_create_capability(principal, ceiling, ask)` is `Some` for the owner at
  Operator ceiling only, `upgrade_home` passes it as `Start.create` beside
  `Start.rename`, and a home that holds either gets `home_owner_accepts` (a
  `submit` beneath `home.table_path`, plus the clicks `home_accepts` takes), one
  rule for both forms; every other home gets `home_accepts`.
  `create_for(standing, tickets, open, create, folder, place, name, sharing,
  within:)` re-derives the page from the grant: `authorized_owner` (open, the
  epoch check, Operator ceiling, the credential authenticates as the page's
  principal and that is the owner), `placed` (a `creations.Place`: a typed path
  is judged by `folder`, which is `new_folder.check` in production; a drawn
  workspace is one the owner's `authorized_page` lists, or one of the owner's
  remembered folders, judged again by `folder`; protocol-change/074),
  `creations.chosen_name` against that workspace, `ui_sessions.reserve_creation` (10 an hour per credential, apart from
  invitations, not given back), then `create` (`HomeAttachment.create`, which is
  `server.create_session`, the control command's own function) under a key
  `web-<hex>`, a `daemon.session_created` line, and `opened_ticket` (the open,
  wait and mint `resume_for` shares). `create_task` runs it in a weft run linked
  to the Lustre runtime and returns at once.
  A folder with no session (protocol-change/074). `client/daemon/new_folder` is
  the filesystem half of the rule for a typed path: `check_in(typed, home)` types
  the text (`creations.typed_path`), expands `~` (`creations.expanded`), makes it
  canonical with `bootstrap.canonical_directory`, requires the canonical folder
  to lie strictly inside the canonical home with no hidden segment
  (`creations.inside`), and requires the folder's owner to be the home's owner
  with owner rwx; `check` reads `HOME`. `server.create_session` ends by
  `manager.remember_folder`, a cast to the registry that writes the catalogue's
  recent-folders table (`catalogue.remember_folder`), so every surface feeds it;
  `manager.recent_folders` and `forget_folder` read and edit it. `recent_for` and
  `forget_for` run `authorized_owner` first and list only entries inside home;
  `home_folders_capability` gives `Start.folders` on the creation capability's
  condition, and `upgrade_home` builds its two functions with `read_task`.
  The home's activity words (protocol-change/065, the home-list addendum) come
  from `HomeAttachment.activity`, which `server.home_activity` builds: the
  control command's `sessions.activity` read over the ids the page's credential
  holds, re-derived in the registry (`held`, `manager.session_authority`) at
  each read, reduced to one state word per session that answered
  (`activity_states`). The control command answers a member the same way, only
  for sessions it holds (protocol-change/050, the addendum on members).
  `activity_task(ask, ids, deliver)` runs it in a weft run
  linked to the Lustre runtime and returns at once, as `resume_task` does;
  `Start.now` is `bootstrap.system_time_ms`.
  The admin page (protocol-change/065, the fifth addendum).
  `home_admin_capability(principal, ceiling, reach, ask)` is `Some` for the
  owner's operating home that `fresh_home(reach)` calls fresh, `upgrade_home`
  passes it as `Start.admin`, and a home that holds it gets `home_admin_accepts`
  (`home_owner_accepts` and a click at `home.admin_path`). `admin_ticket_for`
  re-derives the page at the press (open, Operator, fresh, credential authenticates
  as the page's principal and that is the owner) and mints an `Admin` ticket with
  `mint_before`; `admin_ticket_task` runs it in a weft run. `upgrade_admin` starts
  `web_view/admin` over the same `websocket` with `admin_accepts` (a click or a
  submit beneath `admin.body_path`). `admin_reading(attachment, open, chosen,
  ended)` is the page's read and frame check (`manager.principal_page`,
  `authorized_page`, `session_member_page`; owner first); `admin_for(standing,
  tickets, open, epoch, address, action)` makes each of the five changes afresh
  from the grant, takes a place from `reserve_invite` for a grant (an invitation,
  a rotation, a role raised to operator) and none for a reduction, and shares
  `invitation` and `give_back` with `invite_for`. `admin_read_task` and
  `admin_task` run them in weft runs linked to the Lustre runtime.
  `ui.link` and a switch build the exchange path with `page.exchange_path`.
  An owner's operator page can also invite (protocol-change/051, the addendum
  on inviting from the session page). `ui_socket.Role` has a third value,
  `Owning`, an operator's page whose principal is the daemon's owner
  (`role_of(attachment)`, from `Attachment.principal.kind`, never from the
  page), and `upgrade` builds a `Transport.invite` for that page only (and `admit` puts it in the transport), which calls
  `invite_for(attachment, tickets, open, address, role)`: the page must still
  be open, the principal must be the owner, the claim address must be known
  (`claim_address(request)`, `ws://` and the loopback `Host` and
  `/v2/control`, `localhost` written `127.0.0.1`), the credential must have an
  invitation left (`ui_sessions.reserve_invite`), and then
  `manager.administer` runs `manager.Invite` for a `guest-` principal in the
  page's own session with a claim from `server.claim_enrollment` that lives
  `invites.claim_ttl_ms`. A refusal that made nothing gives the reservation
  back and an unknown outcome keeps it. The socket's admission is split:
  `operator_accepts` drops an event at or beneath `component.invite_path` or
  `component.rename_path`, `owner_accepts` admits them, and the observer's
  socket admits neither. `start_page` takes the `Role`.
  An owner's page can also make its private session shareable
  (protocol-change/065, the addendum on making a session shareable):
  `shareable_capability(role, origin, ask)` is `Some` for `Owning` on a page a
  `loom ui` exchange opened, and `shareable_for(attachment, origin, open)`
  re-checks the origin, that the page is open and `Owning`
  before `client/daemon/shareable.make` stops, isolates (`manager.isolate`) and
  resumes the session, refusing as the module's table says. The admin page asks
  the same task through `admin_for` (`grants.MakeShareable`), which takes the
  daemon's `state_root` (carried on `server.AdminAttachment` and
  `server.Attachment`). Both run in `detached`, a plain `spawn_unlinked` (no weft
  shape fits an unlinked run), because the stop ends the page a linked run would
  die with. A bookmark cannot mint access: `invite_capability`,
  `shareable_capability`, `invite_for` and `shareable_for` all refuse a page a
  bookmark opened (`mints_access`), and its Session tab draws a sentence
  (`invites.Bookmarked`) in place of the controls.
  `rename_for(standing, open, epoch, target, name)` is the owner page's rename
  (protocol-change/067): the page open, the ceiling operating, the credential
  authenticating as the page's principal and that principal the owner, `target`
  a canonical identity, the trimmed name through `catalogue.display_name`, and
  then `manager.rename` (owner and epoch again, and the catalogue refuses an
  identity it does not hold). Every non-owner standing, forged identity and
  unknown session is `renames.NotOwner`; a bad name is `InvalidName`.
  `rename_task` runs it in a weft run linked to the page's runtime, as
  `resume_task` does, and `deliver` is its last act. The session page passes the
  attachment's own session id; the home passes the row's, and is handed the
  capability only for the owner on an operating page
  (`home_rename_capability`), with `home_owner_accepts` admitting a submit
  beneath `home.table_path` for that page alone.
- **Stopping, archiving and deleting from the home** (protocol-change/065, the
  addendum on session actions). `home_manage_capability` hands `ask` to the
  owner's operating home that a `loom ui` exchange opened (`fresh_home`, as the
  Admin button), and `manage_for` re-derives it at the press: the page is fresh
  and open, minted to operate, its credential authenticates as the owner
  (`owner_operating`), the target is a canonical identity. A stop is
  `manager.stop_session` then a `weft/poll` wait of `stop_wait_ms` for the
  registry to hold the session saved; an archive is `manager.set_visibility` and
  a delete `manager.delete_session` (owner and epoch checked again in the
  registry's turn; `AdminBusy` is `actions.Running`). Every other refusal is
  `NotOwner` or `Unavailable`. `manage_task` runs it in a weft run, as
  `rename_task` does. `HomeAttachment.sessions_directory` is what a delete
  removes from. No admission is added: the buttons' clicks are beneath
  `home.table_path`. `admin.Start.ends_at` is read once in `admit_admin` from
  `open()` (the table's monotonic clock) and carried to the wall clock. The
  sidebar's archive action (protocol-change/065, the addendum on archiving from
  the sidebar) adds `actions.StopArchive`, which `manage_for` runs as `stop_for`
  then `archive_for` in the one task, and hands the session page the same
  capability (`Transport.manage`, made in `upgrade` by `home_manage_capability`
  over `page_standing`), so a bookmark's page, a member's and a page of one session
  (`OneSession` reach) have none. `server.Attachment` gains `sessions_directory`
  for it.
- **A session's subtitle.** The hub reports the first accepted human prompt on
  the main strand through `Options.first_prompt` (`gateway.with_first_prompt`,
  filled from `serve.Settings.first_prompt`, which `daemon/main` sets):
  from the direct admission in `prompt_message` and from `admit_held` for a held
  prompt or steer, once, as typed before skill expansion, and never for a
  message with no text. The callee is `manager.seed_subtitle`, a cast to the
  registry that calls `catalogue.seed_subtitle` in its own turn. `server.view_json`
  adds `subtitle` only when present (protocol-change/067).
- `daemon/shareable`: `make(registry, digest, epoch, state_root, id)` makes a
  private session shareable as stop, `manager.isolate`, resume, for the owner
  only (it authenticates before the stop, which takes no caller). A saved session
  is isolated and left saved; each refusal (`NotOwner`, `NotFound`,
  `Unavailable`, `NotStopped`, `NotMoved`, `Stranded`, `NotResumed`) leaves the
  session in the state its module table names, never half-isolated.
- `daemon/ui_relay`: the page's stand-in for a session socket. `start`
  returns before the attach, which runs as the relay's first message and
  answers on the component's `opened` subject, so a slow gateway cannot
  hold Lustre's one-second start. It attaches with
  `attach_authenticated_flushing`, its own pid as the socket, and a binding
  and `check` capped by `capped(membership, ceiling)`: the least of the
  membership, the ceiling and Operator, never Owner. The check is
  `while_open(frame_authority, still_open)`, so revocation, expiry,
  replacement and a role change all end the page. It ends on `Shut`, the
  component's exit, the gateway's exit and the gateway's `close`,
  detaching in each; `ui_socket` closes a quarter second after the
  gateway's end so the ended state is drawn. Its reasons are
  `web_view/ending` strings: the gateway's exit is `SessionStopped`; a
  `close` (or a request the gateway answered as closed) makes the relay ask
  the attachment's check again, and a UI session that is gone is `PageEnded`
  (expired, or displaced as the oldest at the page bound)
  where any other refusal or a changed capped authority is `AccessRevoked`,
  and a check that passes unchanged is `SessionStopped` (the gateway closed
  for its own reason; a failed request with an unchanged check is
  `ConnectionFailed`). `ended` carries an `Ending`, not a string; a refused attach is `NotOpen`
  unless the check named the page's end, and it now also calls `ended`, so the
  socket closes. `server`'s refused page requests answer with
  `page.refusal` (`refused_page`) under the status they always had; the
  session identity in it is drawn only if it parses as a canonical one.

## Purpose

The single daemon and ClientGateway: one listener manages independently
owned sessions. It owns the Part 1.6 websocket protocol as total Gleam codecs,
a per-session hub actor that speaks it to any number of attached
connections, the `mist` websocket transport under that hub, the bridge
that decodes stored escalation JSON back into typed broker grants, the
scripted end-to-end demo that drives the whole M3 flow through the
protocol alone — and, as the tree's host package, the production wiring
adapter (`client/wiring`, promoted from conformance), the system
prompt's assembly and its durable pin (`client/system_prompt`), the four
seams that give a model other agents, code mode, recall over the
repository's own history and a memory that outlives a session
(`client/agency`, `client/codemode`, `client/history`, `client/memory`),
the distillation pipeline that fills that memory
(`client/distill`, a second entry point), plus the
`loomd` entry point (`client/daemon/main`). Startup restores the private
catalogue without opening runtimes. Explicit admission invokes
`serve.resolve_managed` and `serve.assemble_in_domain` for one saved registration.

## Key Types

- Daemon archive and restore requests enter `manager.set_visibility`, whose
  serialized dispatch reauthenticates the owner, checks the daemon epoch, and
  refuses any live runtime slot before changing catalogue visibility. Archive
  listing is owner-only. Admission checks visibility before domain acquisition
  or runtime assembly, including retries with an existing creation key. Restore
  returns saved metadata without opening a runtime. The existing permanent
  deletion command retains its file-removal contract.

- `client/session_git.prepare` writes the reserved `session/git-start` record
  before the runtime writer starts. First activation probes HEAD through the
  read-only worktree broker; restarts reuse the session/workspace-bound record.
  Existing prompts without a baseline and inherited records from another
  session remain unavailable. Store failures refuse boot; Git failures become
  durable unavailable baselines. A web page's Changes tab reads the same
  observation without the gateway: `serve.Instance.worktree` (projected to
  `Resident`) is the closure the gateway runs, `ui_socket.worktree_capability`
  hands it to an owner's or an operator's page only, and
  `ui_socket.worktree_answer` re-runs the page's check, caps by the page's
  ceiling and Operator, and runs it for Operator or Owner alone, inside
  `worktree_task`, a weft run that returns at once (protocol-change/051, the
  addendum of 2026-10-05). `worktree_diff.capture_since` adds a bounded
  commit patch stream, including first-parent merge resolutions, to the same
  owner-scoped observation and budget. Protocol 029 defines this optional field.

- `provider_relay` retains its initiating deadline, cancellation receipt, or
  observer exit in `Relay.context` through the existing cancellation and proof
  states. Startup refusal records cancellation receipt too. Forwarded failures
  retain protocol 031 context; no diagnostic can certify owner retirement.

- Failed or stopped Agency waits preserve their terminal outcome and expose
  saved observations as explicitly labelled partial reports. A missing final
  answer can fall back to at most eight note excerpts. Notes remain mutable
  strand state and are never attributed to the just-finished operation without
  evidence; the result still carries the original structured notes separately.

- `client/skills` serves metadata pages from the assembly's immutable
  `host/skill.Catalogue` and expands selected skills before prompt admission.
  `client/skill_tool.tools` contributes `load_skill` through the existing
  built-in contribution and deactivation path. Its definition advertises only
  names and descriptions; its result discloses the selected complete document.
  `gateway.Options.skills` carries the same capture to the human ingress.

- Daemon control `RenameSession` calls `manager.rename`, whose `Rename` message
  reauthenticates owner and daemon epoch before `catalogue.rename` in the same
  serialized dispatch. Its reply is a `View`; no runtime is opened or stopped.
  [Protocol 019](../../protocol-change/019-session-display-names.md) keeps mutable
  display labels separate from immutable creation-key metadata.

- `client/daemon/admin.Request` is the one-shot `loomd access` command,
  now an alias of `host/access.Request`: the grammar, the exchange and the
  printed lines live in `host/access`, which `loom access` runs too
  (protocol-change/053 phase 2), and `admin` adds the daemon's own decoder as
  a further refusal and the exit status. It reads existing private
  endpoint/owner-token records, verifies the hello epoch, and sends one
  control command through `host/websocket`. It never starts a daemon or opens
  a conversation. The caller's bounded principal ID is printed before
  sending. `list` and `show` send the owner-only `principals.list` and
  `principals.memberships` (`protocol.ListPrincipals`,
  `PrincipalMemberships`), which `manager.principal_page` and
  `membership_page` answer after authenticating the owner in the registry's
  own dispatch; `server.bounded_rows` keeps a page to 60,000 bytes on a whole
  row. `invite` and `rotate` take `--ttl`,
  `--claim-addr` (checked by `host/claim.remote_address`) or the exclusive
  `--credential-digest`; their success prints the claim token only on
  standard output, with a `claim_command` naming the address and never the
  token (protocol-change/053). The reply is re-encoded from checked fields, so
  no other field, and no bearer, can be printed. A timeout does not trigger a
  retry.
- `client/daemon/server` serves a third route, `/v2/claim` (protocol-change
  053). `claim_route` hashes an exact `Bearer loomclaim_<64 hex>` header, asks
  `manager.claim_known` (exists and not void, else 401), and takes
  `root.acquire_claim` (409 while another upgrade for the same claim is open).
  The socket accepts one message of `protocol.max_claim_bytes`, sends a
  `hello` with only `protocol`, closes after `claim_idle_ms` (2 s) without a
  command, and closes after answering its one `credentials.claim`, whose
  optional `name` rides `ClaimRequest` and `manager.claim` to the catalogue,
  and a refused one is `invalid_name`
  (`protocol.decode_claim`, which refuses every control command, as
  `protocol.decode` refuses `credentials.claim`). Invitation and rotation mint
  the claim with `host/claim.mint_token(token.production_entropy())`, store
  its digest with a `bootstrap.system_time_ms` expiry, and reply `claim` and
  `expires_in_ms`; no control reply carries a bearer.
- `client/daemon/peer_cli.Command` provides `loomd peer inspect`, `link`,
  `unlink`, and `send` over that same private owner control transport. It
  requires explicit source and target coordinates, wake policy for links, and
  a stable caller-chosen message ID for sends. It emits JSON with the exact
  coordinates and distinguishes refusal, unknown outcome, and partial
  recipient revocation by exit status. Local discovery errors are classified
  as not sent, and inspect follows bounded pages before returning one result.
  It never starts a daemon.
- `client/daemon/manager.Administration` carries digest-only invitation,
  membership, and principal-scoped credential changes. `Invite` and
  `RotateMember` carry a `storage/access.Enrollment` (a claim digest with its
  expiry, or the invitee's credential digest). `administer` checks
  phase, current owner credential, and epoch in the same serialized dispatch
  as the DAL mutation. Existing IDs conflict, revoked digests stay tombstoned,
  and owner credentials are excluded from member rotation/revocation.
  `manager.claim` runs `access.claim` in its own serialized `Claim` message
  with a constant-time digest comparison (`broker/internal/ffi_crypto`), and
  answers `ClaimRefused(access.ClaimRefusal)` or `ClaimUnavailable`. Both
  `Administer` and `Claim` drop the frame-authority memo before replying, so
  the memo's single-writer argument covers the claim's credential insert.
  Invitation and membership-upserting role changes also require the persisted
  `storage/domain.SessionOnly` scope. `isolate` checks owner, epoch, and absence
  of every retained runtime slot before changing metadata; the wire requires
  explicit acknowledgement that the existing transcript will be shared.
  The domain record captures configuration at creation, not first admission.
- `client/daemon/manager.delete_session` is the only path that destroys
  durable conversation data. It checks owner, epoch and the absence of a
  runtime slot in one serialized dispatch, answers `AdminBusy` — `busy` on
  the wire — when the session is anything but saved, removes the
  registration with its memberships, workspace default and domain mapping in
  one catalogue transaction, and only then unlinks the database and its
  `-wal`, `-shm`, `-journal` and `.tmp` siblings. The durable rows go first
  on purpose: an interrupted delete must leave files nothing refers to, never
  a registration whose database is gone. The domain and everything distilled
  into it survive, because a workspace's memory outlives any one
  conversation. `protocol-change/019` and `docs/client-protocol.md` §3.16
  carry the ruling.
  These metadata checks do not substitute for mapped resource and recall
  admission in the assembly layer.
- `client/daemon/main.{Config, Serving}` selects daemon-wide state, loopback
  binding, capacity, and lazy session defaults. The binary rejects the removed
  per-session flags; `ext` dispatch remains in `client.gleam` to avoid a cycle.
  The `--help` and `-h` flags anywhere in argv — after daemon flags as
  readily as first — and the bare word `help` in first position are
  dispatched in `client.gleam` before daemon startup,
  with the first `access`, `peer`, or `ext` word choosing the topic. `help` in any
  other position stays a value: principal and display names are free-form.
  Help prints
  usage to stdout and never creates state or binds a listener.
  Production startup reserves its OS PID/birth through `host/endpoint` before
  opening the catalogue, then publishes the actual port and epoch only after
  listener readiness. Shutdown retains the endpoint until the VM departs.
- `client/daemon/root.Root` retains the private catalogue, stable `owner.token`,
  kernel lock, registry witness, and original listener-owner monitor. Its
  `start_listener` publishes a parked `client/daemon/listener.Listener` before
  `begin` releases socket acquisition. `control_state` permits existing control
  sockets to inspect drain progress without widening `ready` or new admission.
  Shutdown enters `Stopping` and runs held-input return in a bounded Weft task.
  `manager.DrainHeld` snapshots resident callbacks; the task invokes them outside
  the registry so authenticated delivery can call `frame_authority` there.
  `root.Returns` consumes the task's final report before session cancellation.
  `ConnectionClass.Claim(claim_digest)` is the `/v2/claim` parser class
  (1 KiB, no delivery allowance). Every reservation passes `admission`, which
  refuses a second `Claim` allocation for the same digest while one exists;
  the allocation already lives from the HTTP request to the socket process's
  DOWN, so that check is the whole one-upgrade-per-claim bound.
  `acquire_claim` reports it as `ClaimInFlight` (409) apart from `NotAdmitted`
  (503). Claim sockets are cancelled with session sockets at drain.
  Task completion never replaces the original lifetime monitor's cleanup proof.
  Connection admission uses startup-owned `[daemon]` limits, defaulting to
  64 owners and 512MiB of accounted payload:
  inbound message limits plus 8MiB of bounded delivery allowance per session
  connection, with no exact BEAM heap/RSS claim. Only the original socket DOWN
  releases a transferred reservation.
- `client/daemon/listener.Bound` carries the selected port and original Mist
  supervisor. `terminate_supervisor` only requests shutdown; the original
  supervisor DOWN supplies retirement evidence. Root shutdown retires sessions
  before closing control sockets and the listener. Lost listener proof retains
  the root's recovery-blocked lock.
- `serve.Settings.domain_paths` carries exact persisted memory and index paths,
  including imported filenames and session-only mappings. The manager captures
  the immutable `Domain` before `instance_host.prepare`, then passes it through
  `Assembly.build` without exposing the catalogue connection. Managed resolution
  uses the session registration's runtime configuration independently of the
  domain's maintenance configuration. `None` preserves embedded fixtures' beside-session
  layout. `serve.build_domain` resolves maintenance configuration independently;
  `serve.assemble_in_domain` receives the original shared services capability.
- `client/daemon/domain.Services` retains one original `history.Shared` and an
  optional parked `distillpass.DomainMessage` owner. `build` publishes Services
  cleanup before starting history and Runtime cleanup before starting cadence.
  The registry's capacity-bounded domain book includes closing and blocked slots;
  waiting sessions consume session slots. `DomainOpened`, `DomainFailed`,
  `DomainRetired`, and `DomainSettled` carry exact domain incarnation identities.
  A normal `DomainRetired` remains authoritative even if a late builder result
  or fault arrives. During ordinary closing it preserves parked waiters for
  replacement; in other phases it stops remaining dependents.
- `client/internal/shared_history` prepares a parked original coordinator,
  then owns its search index and at most one read-only history source. Source
  metadata and bounded descriptor/fragment reads advance between actor turns.
  `history.seam_for` binds calls to the current session, while
  `supervised_shared_commit_pull` forwards session-bound commit hints. Fresh
  source authorization precedes ranking and exact reads; stored index locators
  do not grant access. Retirement succeeds only after actual close results and
  the original owner exits normally. Failed close retains the owner and handles.
  A call arriving while the owner is opening its index, or while it is indexing
  a commit hint of its own, is held in a waiting list of at most eight and
  admitted the next time the owner reaches `Ready`; the terminal phases
  (`StartupFailed`, `Blocked`, retirement) drain that list with their own
  reason. Only a call arriving while *another caller's* request is in flight is
  refused immediately. Replies carry `tools/history.Refusal`, so the arm that
  decides a refusal also decides its model-facing sentence: `IndexNotReady` and
  `IndexBusy` say the request was fine and to send it again, and only
  `IndexRefused` blames the request.
- `manager.Summary` reports session occupancy separately from `domain_capacity`,
  `domain_occupied`, and `domain_blocked`. Saved session metadata does not prove
  domain retirement. An open against a normally closing domain reserves a
  parked session and immediately returns `Opening`; it consumes session
  capacity while the original domain continues to consume domain capacity.
  Only the original witness's normal exit permits a replacement domain to
  start. The session operation selects replacement services after publication;
  cancelled waiters never start, and cleanup failure cancels all waiters while
  retaining the domain reservation. After the last clean session retirement, `notify_closed`
  precedes `quiesce` from the same sender; the registry cancels the domain host
  only after current and coalesced maintenance settles. A domain fenced while
  admission is open is revived by the next open in the same domain, taking
  back the same services with the cadence un-fenced along with them
  (`domain.resume`), so a reopened workspace goes on distilling. Resume
  withdraws the fence: the worker drops its parked replies, and the registry
  replaces the settle subject. A reply already decided before resume can
  still arrive, but the replacement selector cannot accept it as the account
  of a later fence. A domain fenced during shutdown is never revived, and
  neither is one whose maintenance
  worker refuses the resume — that refusal fails the domain, exactly as a
  refused quiesce does. An assembly fault reports `Stopping`
  because Weft's ordered cleanup has already begun; only a cleanup failure
  reports `RecoveryBlocked`, so `Summary.blocked` counts custody that will
  never release. Failed cleanup retains admission capacity and prevents
  normal daemon shutdown.
- `manager.Status` joins the live slot with the durable registration, and both
  halves are load-bearing. Where there is no slot the catalogue state is the
  answer: a record still `catalogue.Reserved` reports `Reserved`, not `Saved`.
  Reading liveness alone made an unreconciled creation — an identity with no
  database behind it — indistinguishable from an openable session in a listing,
  and every selection of one was refused `NotInitialized`. `tui/daemon/selection`
  now declines such a row locally and names the create retry it needs.
- `manager.StartFailed` is distinct from `manager.StaleOperation`. A build that
  returns an error has its slot deleted the moment ordered cleanup drains,
  usually before the requesting terminal's first `operations.get`, so the read
  would otherwise be answered "some replacement overtook you" for an open that
  never started. `Book.failed_operations` remembers the failing operation per
  session — one entry each, capped at the slot limit, dropped when that session
  opens again — and the reason itself is deliberately not kept: it can name the
  session path, and `daemon.session_start_failed` already carries the class.
- `client/daemon/main.start_class` classifies a start failure into a stage, a
  class, and the structured detail that class permits. Only values proven free
  of a path or a credential may be logged, which is why the reason string is
  matched rather than recorded. The one exception is an `executor_unavailable:`
  reason: `executor_detail` logs it as `reason` (clipped to 512 bytes) unless it
  contains a path separator, in which case the class alone is logged, because
  a storage error and an executor's own sentence can name a path.
  `serve.storage_open_refusal` renders a held
  writer lease with its expiry so the classifier can emit class `lease_held`
  with `lease_expires_at_ms` — after an unclean exit that expiry is the entire
  answer to when the session reopens. See
  `docs/architecture/sessions.md`, "Reopening after an unclean exit".
- `client/daemon/manager.{frame_authority, FrameRefusal}` — per-frame
  authorization is one registry dispatch that answers the daemon epoch, the
  session incarnation and the session authority together, widest fence
  first, and is still a live read on every check. The four refusals
  (`StaleEpoch`, `StaleIncarnation`, `Unauthorized`, `RegistryUnavailable`)
  stay distinct so the transport never reports a revocation as an outage.
  The control listener keeps using `session_authority`.

- `client/internal/instance_owner.{Owner, Part, CloseOutcome}` retains
  published cleanup independently of a builder. Weft orders builder exit
  before the holder's cleanup run; `StillClosing` and `RecoveryBlocked`
  retain reservations rather than authorizing replacement. Cleanup order is
  `Runtime`, `ToolConfig`, `Services`, `Broker`, `Helpers`, `Mcp`, `Storage`,
  `Namespace`. The publication
  handoff in `instance_owner.start` and the registry handoff in
  `lifetime.start` are both bounded at five seconds, and `distill_owner`'s
  cleanup run carries a wall deadline, so a wedged start or close settles
  with a verdict instead of holding the registry forever.
- `client/internal/instance_host.Host` separates parked preparation from
  assembly. The long-lived registry must call `prepare`, retain the witness
  and monitor it before `begin`; Weft scope lifetime includes its creator.
  The builder remains alive after a successful open. The daemon entrypoint
  uses these owners through `manager.Assembly` and `serve.assemble_in_domain`.
- `client/protocol.{CommandEnvelope, Command}` — the client→server
  envelope `{v, id, cmd, body}` and its seventeen commands (`Subscribe`,
  `CatchUp`, `Prompt`, `PromptContent`, `Steer`, `FollowUp`, `Abort`,
  `Approve`, `Deny`, `Fork`, `Navigate`, `Compact`, `CreateStrand`,
  `ListModels` (wire name `models`), `SetConfig`, `ListSchedules` (wire name
  `schedules`), `CancelSchedule` (wire name `schedule_cancel`)) plus `UnknownCommand`,
  which keeps an unrecognized name as data. `HistoryLineage(from)` (wire name
  `history_lineage`) is the strand-scoped history read (protocol-change/072): it
  begins a bounded transfer (`transfer.Lineage`, step `ReadLineage`, checked by
  `accept_lineage`) of the records on the path from one entry down its parent
  links, at most 100 and 2 MiB, oldest first, from `snapshot_reader.lineage`. A
  client names a strand by its leaf and pages by the parent of the oldest record
  it holds, so the transfer has no cursor (`more_after` is null). It is
  read-only, so an observer's attachment may issue it, and it is authorized per
  frame as `history` is.
- `client/protocol.{EventEnvelope, Event}` — the server→client envelope
  `{v, reply_to?, event, seq?, body}` and its events (`SnapshotEvent`,
  `EntryEvent`, `OpTransitionEvent`, `StreamDeltaEvent`, `UsageEvent`,
  `UsageObservationEvent`,
  `EscalationEvent`, `StrandResultEvent`, `ErrorEvent`, `UnknownEvent`),
  with `Snapshot`, `Strand`, `LiveOp`, `EntryRecord`, `EscalationRecord`,
  `Denial`, and `ModelInfo` as the body shapes (`ModelsSnapshot` is the
  `models` command's reply).
- `client/protocol.{ListSchedules, CancelSchedule, SchedulesSnapshot,
  ScheduleInfo, ScheduleWake}` — the operator's scheduling surface,
  `protocol-change/013`. `schedules` `{}` lists every schedule the
  session holds — operator `[[schedule]]` tables first, then every live
  model-created cell — as a `snapshot` mode `schedules`; `schedule_cancel`
  `{target, name}` retires one model-created schedule and replies with
  **the listing as it stands after the cancel**, so a client re-renders
  from one round trip. A row is `{name, target, owner, when, wake, fired,
  body}`, every field always present: `{target, name}` is a schedule's
  durable identity, which is why both are required on a cancel; `owner`
  is the wire string a client renders (`"operator"`, or the owning
  strand's name) rather than a discriminated type, because the host knows
  which kind a row is and a client cannot; `when` is
  `scheduleseam.describe_timing`'s rendering, printed verbatim and never
  parsed; `wake` is the wire's one boolean here, and in Gleam it is
  `ScheduleWake { WakesIdle | SteersOnly }` with the boolean confined to
  the codec — 012 declined the "frozen contract" escape for a `Bool`
  while the field was still being minted. Refusals: an operator table is
  `conflict` naming the configuration file and the restart; nothing live
  is `bad_request` (a name never used and one already cancelled are the
  same absence, since the store keeps no tombstone); a hub with no
  scheduling plane answers `schedules` with an empty listing on the
  `models` posture and `schedule_cancel` with `unsupported`, because a
  cancel that cancelled nothing must never read as one that worked.
- `client/protocol.ProtocolFault` — what a malformed frame decodes to;
  nothing here crashes.
- `client/gateway.{Gateway, Options, Message, start}` — the hub actor,
  one per served session, over a `runtime/api.Runtime`.
  `with_catalog` and `with_registry` supply the two things `set_config`
  validates against: the model catalogue behind `model_name`, and the
  registered-name projection behind `active_tools` (derived from the same
  registry the effect wiring dispatches through; without one, active-set
  changes are refused in band).
- `client/gateway.{attach, detach, handle_text}` — the transport seam: a
  connection is a `fn(String) -> Nil` sink, inbound frames arrive as
  text, and nothing in the module knows about sockets. `attach` returns
  `Error(Nil)` when no hub incarnation is bound; the WebSocket transport
  closes that attachment rather than retaining an invented connection ID.
- `client/gateway.{commit_forwarder, supervised_commit_forwarder,
  tap_provider, tool_output_observer, with_bus}` — the composition-layer
  seams: the runtime writer's post-commit publication becomes a pull
  hint; an injected `effects.ProviderSurface` is wrapped so provider
  deltas tee to the hub while the runtime's effect process consumes the
  stream unchanged; and `tool_output_observer(bus, session)` is the
  `wiring.Config.observe_output` resolver that publishes each running
  call's output window on the bus as `ToolOutput` under the session's
  canonical id, which the hub — given the same bus by `with_bus` — pushes
  to subscribed peers as `tool_output` (`protocol-change/031`). The
  wrapper forwards explicit cancellation and monitors that effect process;
  either cancellation or consumer death tears down the inner handle. The
  forwarder binds a reclaimable Weft reference address. The writer's
  `Routed` subscription resolves it for each hint, so a restart neither
  requires resubscription nor interrupts the writer.
  `tap_provider_with(surface, to:, also:)` is `tap_provider` with a second
  observer factory on the same relay: `also(spec, generation)` is asked
  once per request with the hub's own request identity, and its callback
  sees every stream event after the hub's. `client/serve` passes
  `blocksummary.observer`, so the live summary feed costs a callback and
  no extra relay processes.
- Provider observation layers retain the inner preparation capability and its
  timeout. `provider_relay.observing` and `previewing` build both outward
  facades from that capability, so adding a layer does not recursively copy
  both facades of every predecessor. Preview factories still execute in the
  observer process. `extension/hooks.wire` similarly captures each wrapped
  hook or tool function before the record update. Copy-size regressions cover
  sibling-slot independence and every provider observation layer.
- `client/provider_relay.{prepare, wrap}` — the shared provider-wrapper
  ownership seam: `prepare` returns a minimal public custodian before it
  releases the guard, while `wrap` is the prepare-and-begin compatibility
  facade. The
  guard remains the inner surface's direct consumer, and a separately
  monitored observer runs the synchronous callback before each event is
  forwarded. The guard installs its drain monitor on the inner owner before
  `begin`, and the custodian
  adopts the guard, leaf observer, and transitive inner stream owner before
  each begins work. A terminal is not forwarded until that original monitor
  reports a normal exit. Cancel travels inward, an unacknowledged cancel
  becomes terminal `CancellationUnconfirmed` after one fixed grace, and the
  custodian remains alive until the registered subtree drains. Guard or
  observer death becomes a prompt in-band transport failure only after that
  drain is proved.
  The guard itself is a **`weft/state_machine`**, started `sm.unlinked` by the
  consumer it serves so a guard crash reaches the custodian's worker adoption
  rather than the consumer's link. Its state ADT is `Phase` — `Parked` (the
  wait for the begin permit), `Forwarding`, `Cancelling`, and the three ways
  this relay ends (`ProvingTerminal` holding the terminal the observer has
  already seen, `ProvingFailure` bounding the drain wait after the relay's own
  worker failed, `Proving` waiting out an answer already sent or deliberately
  withheld). Its data is `Awaiting` before the permit and `Relaying` after it,
  which is where everything that moves per event lives: the outstanding
  observation, the events queued behind it, and the inner owner's drain proof.
  Both deadlines are therefore structural rather than scheduled: the
  cancellation grace is a **state timeout** on `Cancelling`, which is what
  makes it one fixed proof deadline a flood of late deltas cannot renew, and
  the request deadline is an **event timeout** re-armed by each forwarding
  step that leaves the guard waiting on the inner stream. A parked guard
  selects three things — the permit, the control subject, its consumer's
  monitor — because the stream's own channels are created by the work the
  permit authorises, in that process; the same step that grants the permit and
  transitions to `Forwarding` or `Cancelling` widens the mailbox with
  `sm.with_selector`.
- `client/server.{Config, Auth, Server, serve}` — the `mist` websocket
  transport on `/v1/ws`; `LocalAuth(token_path)` mints a startup token
  into a `0600` file, `BearerAuth(token)` is the caller-supplied one. The
  whole surface is `@internal`: it attaches through `gateway.attach` with
  one shared bearer and no principal or role, which protocol-015 leaves no
  production route for, so only this package's own tests may start it. The
  daemon listener in `client/daemon/server` is the production route.
- `client/install.{root, helper, helper_name, gleam_compiler, erl, seed,
  seed_directory, existing_file, existing_directory, bundled_helper,
  bundled_seed, first_of}` — where this Loom is installed, and what ships
  beside it. `root()` is `code:root_dir()`: the unpacked release tree for
  a release, the OTP installation root for every other run. Everything
  else is a path built onto it and *probed*, so existence is the whole
  discriminator and nothing special-cases a development host. `erl()`
  names `erts-<vsn>/bin/erl` with the version read from the live system
  rather than globbed for, so it is the emulator actually running the
  harness. `first_of` is the ladder itself — the first rung that answers
  wins, and no rung below it is evaluated.
- `client/grants` — decoding the runtime's opaque escalation JSON back
  into `broker/policy.Grant`, and encoding it again. Pure and total.
- `client/escalate.{Config, Message, Refused, Decision, Escalations,
  seam, start, none, record_id, action_digest, action_preview,
  default_config}` — parking: what a production tool call does when the
  broker refuses it on policy. `seam` closes over a process *name* (the
  Agency's knot, the Agency's answer) and `start` puts the runtime
  behind that name after `api.open`; `record_id` is the deterministic
  `{strand, tool, wanted diff}` digest a retry loop dedupes onto, with a
  limit grant contributing its field and not its model-supplied
  magnitude; `action_digest` is the *other* identity, over the call's
  effective arguments canonicalised (objects key-sorted recursively,
  arrays left ordered), which is what an approval binds to;
  `action_preview` is its bounded human rendering, display-only and cut
  at 2 KB on a codepoint boundary; `Config.max_records` caps the
  distinct wants a session will file and `Config.max_asks` caps the
  questions any one of them may put; `none()` is the no-plane seam under
  which a refusal settles exactly as it did before escalations existed.
- `client/gateway.attached` — how many connections are attached right
  now. The honest answer to "is a human there?", which is what the
  escalation seam asks before it holds a call open. A hub that is gone,
  that dies mid-question, or that does not answer inside a second all
  count as nobody attached rather than exiting the asker.
- `client/catalog.{Catalog, CatalogModel, Dialect, parse, gateway}` —
  the model catalogue: a total, strict parser for the `loom.toml`
  format (via the `tom` TOML package; `docs/examples/loom.toml` is the
  worked example) and the builder that turns a catalogue into the
  provider gateway's registry — one provider per entry, named by the
  entry (so durable identities store `{catalogue-name, model_id}`),
  one route per `[roles]` row, a positive per-model `max_images` limit
  (default eight), and one rate card per entry that carries
  an optional `[models.<name>.pricing]` table (US dollars per million
  tokens; `input` and `output` required, the two cache rates defaulting
  to `input` so the default over-reports rather than hiding spend). An
  entry with no table is unpriced and its usage records keep the zero
  cost the adapters write. The hub serves it as the `models`
  listing and resolves `set_config`'s `model_name` against it; `serve`
  loads it from `--config` or shapes a one-entry catalogue from the
  `LOOM_*` environment. `Catalog.lsp_servers` is decoded by
  `client/lsp/profile.decode_servers`, not here: `catalog` checks only
  that `[lsp]` is a table and hands over its entries.
- `client/secrets.{Source, Entry, Failure, Capture, Runner, parse,
  resolve, store, host_runner}` — the `[secrets]` table of the same
  `loom.toml`: how the daemon *obtains* a named credential the operator's
  shell never exported. One source today, `Command(argv)`, run on the
  host and outside every jail with the daemon's own environment, bounded
  by `default_timeout_ms` (10s), stdout less one trailing newline as the
  value — and empty stdout is a failure rather than an empty value, so
  such a name keeps falling through to the environment. The entries run
  on each session create and open rather than once per daemon, so a
  rotated token is picked up with no restart, at the price of one serial
  pass over the entries per open. `store` layers the resolved pairs over a
  base `provider/secret.SecretStore` — resolved name wins, everything
  else falls through to the process environment — and that layered store
  is `Settings.secrets`, the one seam `api_key_env` (models and MCP), an
  extension's bound egress secret, and `[tools] env` all read. A failed
  entry is a `secrets.unresolved` warning carrying the name and an exit
  status, never output and never a refused session.
- `client/catalog.OpenAiResponses` — the `openai-responses` dialect requires
  `auth = "api-key"` and a nonempty `api_key_env`, and defaults to
  `https://api.openai.com/v1`. The existing `openai` spelling remains
  Chat Completions. Older dialects still refuse `auth`; all API-key entries
  refuse `profile` and arbitrary `headers`.
- `catalog.CodexSubscription(profile)` requires `auth = "codex"` and a
  bounded ASCII profile name for the compatibility `codex-subscription`
  spelling. It rejects `api_key_env`, `base_url`, and arbitrary `headers`.
  `catalog.gateway` registers a
  `provider/gateway.CodexSubscriptionProvider`; `serve.adapter_api` keeps the
  distinct `"codex-subscription"` durable identity. Inference uses the fixed
  public Responses API with the native SIWC grant. Authenticated `/v1/models`
  exposes account-visible IDs only; context and reasoning properties remain
  catalogue configuration.
- `client/codex/profile_control.{Command, ControlEvent}` is the native operator
  vocabulary, passed over an in-VM `Subject` and never over a wire.
  `Status`, `LoginBrowser`, `Logout`, and `Models` enter
  `codex/transport.command`; the CLI monitors its parked `http.PreparedRequest`
  before calling `begin` and accepts completion only after normal owner drain.
  Events are closed types: `Permission` (`PlanEnabled`, `IdentityOnly`) and
  `SignIn` (`SignedOut`, `SignedIn(permission)`) replace status strings.
  `LoginInstructions` carries the bound loopback port, and the CLI builds the
  literal `/auth/start` URL from it with `oauth.start_url`, never the issuer
  redirect or returning identity hint. There is no device command.
- `client/codex/transport.{transport, command, prepare_with}` owns each native
  operation in one witnessed managed task. Its worker creates and publishes
  its own begin subject before preparation returns. The same `weft.Ledger`
  covers authentication HTTP, the callback listener, and inference HTTP;
  every owner is adopted before admission and its original drain witness
  remains required after cancellation or worker loss.
- `client/codex/credentials.{Binding, Grant}` keeps issued client, stable host
  URI, verified subject, and optional grant in a bounded private versioned
  record. `profile.{auth, execute}` acquires an existing kernel launch lock
  for each bounded credential operation, rereads the record, and saves
  rotation atomically. It releases the lock before inference starts, permits
  several profiles without a VM-global token cache, and allows control while
  a stream is active. Login holds the lock for the browser attempt; contending
  operations can report `profile_busy`.
- `client/codex/oauth.{Services, Attempt, Callback}` owns browser PKCE,
  dynamic-to-issued client registration, state, nonce, refresh and revocation.
  `oidc.Verification` distinguishes browser nonce validation from refresh
  subject continuity. Gose pins RS256 to the fixed issuer JWKS and validates
  issuer, issued-client audience, expiry and subject; multiple audiences
  require a matching authorized party. Identity-only grants are saved,
  including grants without a refresh token, while plan inference requires
  `resource.invoke` and `chatgpt.tokens.use.direct`.
- `client/demo.run` — the M3 acceptance flow end to end, executed as a
  test and runnable as `gleam run -m client/demo`.
- `test/client/tui_e2e_test` + `test/support/terminal` — the real
  `loom` binary against a real `serve.boot`, in a real terminal
  (`tmux` on a socket minted per run from the VM's OS pid, declared
  geometry, every wait a predicate over pane content with a deadline). Neither
  side of the protocol is fake; only the model is scripted. The socket name
  must stay unique per run: `tmux -L` sockets live in one per-user directory
  shared by every checkout, and a fixed name once let a concurrent run's
  `kill-server` end this one's pane (a `/dev/ttysNNN: No such file` line in
  the TUI's stderr) or leave its keys and captures following the other run's
  session.
  Skips — loudly — when `tmux` or `go` is absent.
- `test/support/tui_driver` is the in-process real-client companion.
  Each actor owns a socket ingress and a separate model inbox, and uses
  `tui.connect_remote` and `tui.run_script` for the shipped handshake and
  loop. Selected socket messages move through the model inbox in order;
  sharing that inbox with the socket would let re-delivery reorder frames.
  `two_virtual_tuis_share_one_real_session_test_` in `tui_e2e_test` checks
  two distinct prompt/reply round trips, equal decoded records once in
  order, rendered replies, fresh-subscription recovery, and both detaches.
  The next prompt waits for the clients' idle phase, not merely the
  assistant entry. `tui` and the existing pinned `etui` are test-only
  dependencies; neither enters the server's production dependency graph.
- `test/support/provider_http.{Exchange, Latest, ObservedRequest, with_server}` is
  a finite loopback Anthropic peer for the separately shipped daemon. It
  checks the latest user text or exact successful tool result before consuming
  one script step, and returns bounded request evidence. `ToolUseExchange`
  emits a fixed call; `ToolResultExchange` requires its nonempty ID and complete
  single text result with `is_error: false`. `ComputedExchange` is the one step
  whose reply is built when the request arrives, for handles the harness mints
  while a fixture already runs — a background job's id lives in the text of the
  tool result that announces it. Its matcher stays exact: either the complete
  user text, or the provider's own invocation identity when the result's text
  is the unpredictable half. Only the answer is computed, and only from
  retained request evidence the caller still asserts on. Text steps retain the normal
  human-attribution projection. Tests pass the actual HTTP tool-use stream
  through the production decoder and encode results through the production
  adapter. Exact refusal reasons distinguish limit checks from script mismatch.
  A refused, replayed or missing request makes the final report fail. The
  wrapper retains original listener and script-actor monitors outside its
  bounded callback. `tui_shipped_multiplayer_test` uses ordinary provider
  configuration and a public dummy key, then compares three native drivers'
  exact durable records, authors, rendered answers and idle completion across
  two turns and an operator's detach/rejoin. No fixture writes the answers
  into the session store or injects a transport into the shipped VM.
  The same fixture checks invitation boundaries against a second resident
  session in another workspace. Owner-positive metadata, operation and upgrade
  checks distinguish authorization refusal from a missing target. A raw valid
  observer `set_config` frame checks the gateway independently of the TUI guard.
  A later owner-acknowledged membership revocation closes Bob's live raw and
  terminal attachments. Surviving clients receive Alice's next configuration;
  Bob retains his old cut and cannot reattach, although control authentication
  still succeeds. This is distinct from the scripted admission/delivery race.
  The real `/sessions` selector also retains the original adopted channel when
  membership in its highlighted target is revoked before Enter. An owner
  attachment proves the target still works; Alice's next shared configuration
  update proves the original attachment and socket still carry traffic.
  After membership is restored, Alice switches A-to-B-to-A while Reader stays
  on A. Two further provider turns establish independent histories, and Alice
  returns to A's original epoch/incarnation with records equal to both peers.
  The turns are sequenced; simultaneous provider execution is not claimed.
  Its later live-tool stage creates A2 in A1's workspace. An ordinary bash
  invocation remains held while Alice first fails to select A2, then completes
  separate A2 and B turns. Host release follows both turns. Exact invocation,
  result and final-answer records converge on the two A1 peers and Alice after
  her return; fresh A2/B captures retain their own histories. This uses all
  eight provider script steps and preserves the original cleanup deadlines.
  Workspace markers prove benign execution, not filesystem confinement.
  `daemon_shipped_stop_test` holds a real HTTP response in A while B completes
  turns before and after A's stop. Exact provider-socket closure precedes the
  owner's `Saved` observation. Explicit reopen resumes A in a new incarnation:
  two equal HTTP requests correspond to one durable user admission, an
  interrupted settlement and a final answer. The fixed held peer owns its
  sockets in one managed worker and routes observations through an actor's
  reply protocol. Framing and closure negatives exercise its actual boundaries.
  This proves cooperative stop/recovery, not an uncooperative drain or a kill.
  `daemon_shipped_schedule_test` changes only a Saved session's configuration
  to add an overdue one-shot. A live peer progresses while a read-only SQLite
  cut proves no firing or transcript mutation. Explicit open fires it once;
  another stop/open preserves its exact durable fired cell and all message
  records. This uses ordinary configuration and the real scanner, not a poke
  or injected clock. It does not cover recurring cursors or memory-file absence.
  `daemon_shipped_jobs_test` is the background-jobs acceptance. A scripted turn
  backgrounds `tail -f build.log`; the fixture appends three lines from outside
  the jail; the next turn's poll must find the job pending; a kill produces a
  terminal state naming the owner and carrying the helper's `cancelled`
  witness, and that same poll — ordered after every byte the helper sent — must
  carry exactly those lines, the staged spill and the cursor they advance to;
  and the payload must be gone. How that last one is proved is decided once
  from the platform (`PayloadIdentity`): a birth-qualified fence over the pid
  the payload publishes where the host shares it, and the terminal record plus
  the jail's containment where `bwrap --unshare-pid` hides it. A second
  scenario runs the same door from code mode through a real hermetic build and
  a real satellite, and proves the durable half of independent lifetimes: a
  later program finds the record under its own id. It asserts no state, because
  a code-mode teardown aborts its whole operation and takes the job with it —
  `docs/next.md` owns that. A third SIGKILLs the VM and proves the sweep
  commits `Lost`, the model's own poll reads it, and the start marker the
  fixture removed stays gone, which only a respawn would undo.
  `daemon_shipped_confinement_test` is the product-level half of the state-root
  masking claim the sandbox self-test probe makes at the helper level. Two
  sessions on one fresh state root: B is created first so its conversation
  database exists, then A runs four foreground `bash` calls, three of them at
  the daemon's own `owner.token`, `catalogue.db` and B's database by absolute
  path, and one that writes a per-run marker inside A's workspace and reads it
  back. The negatives assert on the secret rather than the error, because a
  masked path does not refuse the same way twice: a protected file is bound to
  `/dev/null` on Linux and reads as zero bytes, a protected directory becomes a
  read-only tmpfs, and Darwin denies all three. So the assertions are that the
  token's own bytes, read from the host, and the SQLite header never reach the
  model, and that B's database open failed or produced nothing. Every command
  ends in an `echo` carrying its own exit status, so a refusal is still a
  successful tool result the scripted provider's `AwaitToolResult` steps match.
  A host-side existence check runs before the negatives, so a moved layout
  fails as "path absent on host" rather than passing vacuously, and the
  positive fails a jail that runs nothing at all.
- `test/support/enforcement.{Enforcement, probe, unavailable_reason}` measures
  whether the `loom-exec` beside a shipped server can enforce a policy on this
  host, by running one harmless jailed command under `PlatformEnforcement`. An
  OS name and the presence of `bwrap` establish nothing. Every shipped fixture
  that needs real confinement gates on it, each passing its own label so a census
  learns which declined, while the reason after the colon stays one string with
  one declaration in `.github/declared-skips-linux-gate`. It panics rather than
  skipping on a missing, unstartable or failing helper: that is a broken
  shipment, not an environment without a jail.
- `client/agency.Config.subagent_model` — the host's `subagent` route,
  resolved, as a closure: `Ok(#(identity, thinking))` seeds a spawned
  child with that model and that level, `Error(Nil)` inherits the parent
  wholesale. A closure for the same reason `clock` and `rest` are — the
  Agency is built before `api.open`. `Config.models` carries the catalogue's
  explicit choices as identity/thinking pairs. An optional spawn `model`
  selects one by catalogue name before consulting this default route.
- `client/agency.{Config, Message, seam, start, reaping_hooks,
  child_name, is_subagent, frame_message, frame_brief,
  child_notice, ChildFacts, result_schema_prefix}` — the Agency:
  `tools/agent`'s messaging seam implemented over a live runtime. `seam`
  closes over a process *name* so it can be built before `api.open`;
  `start` puts the returned runtime behind that name. Everything with
  teeth lives here — the descendant-only addressing rule, the depth and
  fan-out caps, the multi-handle wait loop, the lineage ledger's four
  reconciliation branches, the lazy deadline reap, the result contract a
  spawn's `result_schema` writes and the child is judged against, and the
  framing that marks another agent's text as data. `child_name` mints
  `sub:{parent}/{slug}-{digest}`, the digest being
  `agent.call_site_digest` over the caller's coordinates and the `Minter`
  inside them. `owns(runtime, ancestor:, strand:)` is the addressing rule
  exposed for a seam outside the messaging plane —
  `client/scheduleseam` asks it whether a schedule's target is a strand
  the caller spawned — and it fails closed at every step: an unreadable
  ledger, a strand with no cell and a cell that will not decode all
  answer `False`, and the relation is strict, so a caller meaning "itself
  or a descendant" says so at its own door. `is_subagent` is not a
  substitute: it says a name was minted by *an* Agency, not by whom, and
  a sibling's name is shaped exactly like a child's.
- `client/codemode.{Config, Toolchain, seam, discover, toolchain,
  install_prefix, toolchain_mounts, clear_of, default_config,
  execute, exec_config, build_config, exec_root, execution_policy,
  sockets_under, socket_directory, host_socket_directory,
  reaching_socket, translate, pooled_budget}` — code mode: `tools/codemode`'s seam
  implemented over the real pipeline, the other package this one exists
  to join (`tools` cannot see `codemode`, which already depends on it).
  `discover` locates `gleam`, `erl` and a verified build seed or says
  what is missing *and how to supply it*, each executable through
  `locate` — the tree this server shipped in first, `PATH` second;
  `seam` closes over a `Config` and needs no process
  name, because the broker it dispatches through already exists;
  `execute` prepares a per-execution directory, drives vet → compile →
  run under the caller's own `{op_id, step_id}`, restates the pipeline's
  two enforcement reports in the tool's vocabulary, and removes the
  directory again. `Config` carries no `vet_policy` of its own: it
  carries `surface`. `clear_of(toolchain, writable_roots:)` refuses a
  toolchain one of whose read-only mounts would cover a writable root,
  `/proc` or `/dev` (`protocol-change/057`). `install_prefix` resolves no
  symlink, so a merged-usr `/bin/erl` found through `PATH` gives `/` and a
  symlinked `~/bin/gleam` gives the home directory; the refusal is the
  choice over resolving the link because resolution needs an `@external`
  and a resolved target can still be `/` or `$HOME`.
- `client/codemode.launch_refusal` — what policy composition would
  refuse a satellite launch for: `tools/codemode.RunRefused` carrying the
  exact grants that would satisfy it, or `NothingRefused`. `execute`
  wraps the pipeline's `satellite.Launcher` with it and reports the
  answer outward beside the outcome, which is the whole of what makes an
  escalation record *mintable* from inside code mode (#97). Public
  because it is the wrapper's entire decision and the only part of it a
  hermetic test can hold still.
- `client/codemode.{workspace_seam, workspace_seam_for, search_seam_for,
  over_scratch,
  over_jobs, into_blobs, blob_directory, default_blob_root}` — the harness-side capability
  bridge (issue #16), and
  the half of it that lives on this side of the seam. `workspace_seam`
  builds the eight closures `codemode/workspace`'s router calls: `fs_read`
  and `fs_list` over `tools/fs.resolve_real` and `fs.read_text_file`,
  `fs_write` and `fs_edit` over `fs.resolve_writable` —
  the harness's *own* path boundary and *own* large-file guard, never a
  second resolution — `kv_*` over the session's `client/scratch` store,
  and `emit` over the session's blob root. `over_jobs` is how a
  `Config.jobs` is set: `Some(door)` becomes the `JobDoor` a program's
  `job.*` calls reach, and `None` becomes `workspace.no_jobs()` — every
  arm refusing in band rather than going unrouted, which is the opposite
  of the `schedules` posture and deliberate, because `bash`'s `mode`
  argument offers a job from a tool call whether or not a program can
  start one. `workspace_seam_for` is the same bridge bound to coordinates
  a caller supplies rather than a request's, which is how
  `client/extension/dispatch` builds an extension invocation's.
  `search_seam_for` is the read-only sibling `codemode/search`'s router
  calls (issue #365), composed under `workspace.routing` on both the
  code-mode and the extension path, and it takes the workspace and
  nothing else — a search reaches no store, no schedule and no job.
  Its four closures differ only in which path they put through
  `resolve_real`: `glob` and `grep` resolve the query's root and let
  `tools/search` keep containment by never following a symlink,
  `read_lines` resolves the whole path exactly as `fs_read` does, and
  `stat` resolves the path's **parent** and lstats the leaf beneath it,
  because a `stat` that resolved the leaf would follow the very link it
  was asked to report. `blob_directory` (`blobs`) is the one
  place the store's name is written down. `client/serve.session_blob_root`
  computes the root once, in the session's state directory outside the
  workspace, and hands the same string to the tool context, to
  `into_blobs` and to the jobs spill, so an artifact a program emits and
  an oversized `bash` output that overflowed land in one store under one
  address. `default_blob_root` (`<workspace>/.codemode/blobs`) is for a
  host with no state directory, which in practice means tests and the
  demo; a daemon never uses it. `legacy_blob_directory` (`.blobs`) names
  where earlier releases kept the store, for `client/blobs` and
  `worktree_diff`. Listing is
  the one operation with no counterpart in the tool set — `tool.FileSystem`
  has no primitive for it — so the enumeration is `simplifile`'s, on a
  path `resolve_real` has already contained, bounded before it is
  classified and answered with lstat semantics so a symlink is never
  called a directory.
- `client/history.{Config, Message, index_file, default_timeout_ms,
  index_beside, probe, over_session, with_source, sqlite_generation, start, supervised,
  stop, poke, synchronize, seam, commit_pull, supervised_commit_pull}` —
  the one process that owns this repository's `events/search` index, and
  the seams that reach it. Addressed by a Weft reference, like the Agency
  and scratch store, so the tool seam is built before the holder exists
  and a replacement binds that address and reopens the index. `probe` is
  the boot question: an index that will not open registers no
  `history_search` tool and logs one worded line, never a boot refusal.
  `over_session` binds the holder to one open session's store and takes
  its rewrite generation as a **thunk** (`sqlite_generation`), called
  fresh on every pull, because a precise rewrite bumps it underneath a
  long-lived holder — and a generation that cannot be read skips the sync
  rather than guessing zero. `with_source` records the host's session path
  after sync. `ReadEntry` resolves canonical IDs through that registry,
  validates source identity and returns a decoded entry through a read-only
  SQLite connection. The model never chooses a filesystem path.
- `client/notes.{max_digest_bytes, fence, digest_hooks, digest, cells,
  try_cells, strand_of}` — the `agent/` notes digest injected at run
  start, and the board read `client/checkpoint` shares. `cells` reads an
  unreadable store as an empty board — right for a digest, which a run
  is never held up for — and `try_cells` is the `Result` form the
  checkpoint asks, because publishing "you wrote no notes" over a store
  that did not answer would be a durable falsehood.
  `digest_hooks` **wraps** an existing `run_start` rather than setting it
  (`hooks.with_run_start` replaces, so a setter would silently drop a
  previous layer), resolves the strand from the `OpId` through the
  durable `op.meta` cell, reads the strand's `agent/{strand}/` fact cells
  straight off the session store — hooks are built before `api.open`, so
  there is no runtime handle and no Agency to ask — and renders them
  newest-written-first by register seq, capped at 4096 bytes, fenced and
  attributed. A strand with no notes gets nothing at all.
- `client/blocksummary.{Route, Wiring, Message, route, key, encode,
  decode, jobs, request, parse, read, observer, start, supervised,
  request_timeout_ms, max_answer_tokens, request_deadline_ms,
  max_summary_bytes, source_limit_bytes, scan_limit}` — the block
  summarizer (protocol 050), one **`weft/state_machine`** per session
  that asks the `summarize` role for a one- or two-sentence summary of
  each long reasoning block and each long delivered advice or nudges
  message. `route` resolves the `summarize` role's first identity and
  pins it (`ForResolved`, thinking off, `max_answer_tokens`) with **no
  fallback** to another role or provider; a catalogue that routes none
  gets no machine. The confidentiality check compares services by
  `endpoint(entry)`: the lowercased scheme and host of the entry's
  `base_url`, or `dialect:<name>` when it has none, so entries on one host
  are one service. `settled_admission(catalogue, route.provider)` is the
  set of entry names sharing the summarize entry's endpoint, and
  `jobs(entry, admits:, floor:)` admits a reasoning block only when it
  accepts the message's `provider` (an unknown name is refused). A stream
  names no provider and a role's chain may fall back across services, so
  `observer(name, admits)` observes a request only when `admits` accepts
  its strand identity and `Turn`; production passes `live_admission(catalogue,
  provider)`, the identities for which `admits_live` holds: the identity,
  every chain it heads, and, for a text-only identity on an `ImageTurn`
  (`vision.image_bearing` of the request's context; a poll counts as one),
  the `vision` chain all share the endpoint. Both sets are computed once at wiring time, so no
  catalogue is copied into a relay process. Any other request gets a
  no-op callback. Advice and nudges bodies
  (`advisorslice.delivered_body`) are exempt. The machine has two
  addresses: `Wiring.name` for the tap's `Streamed`/`StreamEnded` casts,
  and `Wiring.commits`, a `writer.Event` address it binds itself in its
  initialiser with `registry.register_self`, which the runtime writer
  names as a `Routed` subscriber — so no forwarder process sits between
  them. A commit hint scans message entries above an **in-memory**
  high-water (set below the first hint's oldest seq, so a restart never
  summarizes history) up to the hint's newest seq, at most `scan_limit`.
  `client/blocksummarybook` paces the work; each launch is a one-task
  `weft` run under `request_deadline_ms` relayed into a per-flight sink,
  cleared only on the run's last word. A settled task asks, bounds the
  answer with `parse` (whitespace collapsed, `Summary:` and quotes
  stripped, `max_summary_bytes` at a word boundary), writes
  `summary/<entry>/<block>` through `Wiring.write`, then publishes
  `bus.BlockSummary(SettledBlock)`. A live task publishes
  `bus.BlockSummary(LiveStream)` and stores nothing. Every failure writes
  and pushes nothing; the first is logged `block_summary.unusable` at
  warning level, later ones at debug. `read(session, blocks)` is the
  `block_summaries` command's exact-key read and returns `Read(board,
  missing)`. **Only the primary strand is summarized eagerly**: a commit's
  jobs are kept when their entry lies within `primary_window` (128)
  entries of `main`'s leaf, and live streams on any other strand are
  ignored (resolved once, then remembered in a set emptied at 32). Other
  strands' blocks are summarized on demand: the gateway, after replying to
  a `block_summaries` read, passes `missing` to `ask_for`, and the machine
  queues each eligible block (the same `jobs` test) that is not already
  waiting or out (`blocksummarybook.queued` plus the settled flights), so
  repeated reads queue nothing more.
- `client/blocksummarybook.{Pace, default_pace, Source, Job, Launch, Book,
  new, streams, tracks, waiting, grow, ended, landed, admit,
  settled_landed}` — the summarizer's pacing as pure functions. Settled
  jobs launch oldest first up to `settled_concurrency` (2) with at most
  `settled_backlog` (16) waiting, dropping the oldest past it. A live
  stream is due when it has no request out, has carried `floor_bytes`
  (512) and has grown `every_bytes` (4096) or `every_lines` (40) since
  its last request began; growth during a request coalesces into at most
  one more request, launched by `landed` with the newest text. A stream's
  retained text is trimmed to its newest `window_bytes` (32 KiB) once it
  reaches twice that. A stream that `ended` with a request out is
  forgotten when that request lands. Across streams, at most
  `live_concurrency` (2) live requests are out, and a stream refused a
  slot is considered again on its next fragment; at most `max_streams`
  (8) streams are tracked, and a new one evicts the oldest with no request
  out, because the relay does not report every stream's end to its
  observers.
- `client/advisorslice.{Bounds, Slice, Moment, default_bounds, render,
  feed_message, advice_message, nudges_message, is_advice, delivered_body,
  advice_header,
  advice_footer, feed_header, feed_footer, nudges_header, nudges_fence}` —
  what the advisor strand is shown of the primary's branch, and the frames
  that carry text both ways. `Moment` is `RunEnded | RunOpen(steps)` and
  decides one leading line of the feed body: a mid-run slice says the run
  is still open and how many steps it has been, because an advisor weighs
  a `block` against work in progress differently from work its author
  considers finished. That line is body rather than a second header on
  purpose — the frame's two tokens are what `is_advice` and the terminal
  recognize a feed by, and the terminal keeps copies it cannot import. `render` turns the entries appended since a stored cursor into
  one bounded text: `Bounds` caps a single entry's payload and the whole
  window separately, oldest entries are dropped from the front with an
  `[N earlier entries omitted]` line, and a tool result too long for its cap
  is cut from the middle so both the command and its verdict survive.
  Assistant thinking never renders, redacted markers included. `Slice.newest`
  is the seq of the newest entry the scan saw whether or not it rendered, so
  an entry the rules skip still advances the cursor. Advice and nudges land
  in the primary's branch as ordinary user messages and come back in the next
  slice labelled `advisor (your earlier advice):` and
  `advisor (your earlier nudges):`, because unlabelled they would read to
  the advisor as operator instructions. A frame is recognized by its header
  *and* its footer, so an operator quoting a verdict back keeps their own
  attribution, and `advice_message` makes the body frame-safe — either
  advice token occurring inside it has its brackets replaced with
  parentheses — so model-written text cannot close the frame early and
  continue in the operator's voice. The module is pure — no store, no
  process, no clock — and borrows `notes.{clip, byte_size, fence_safe}`
  rather than keeping a second copy of the byte arithmetic and the fence
  defence.
- `client/advisorguard.{Verdict, Policy, Guard, Decision, default_policy, new,
  runs, pending, primary_run_ended, decide, take_pending, encode, decode}` —
  what one `advise` verdict becomes, as pure data. `decide` applies four
  rules in order: empty text is dropped, advice whose normalized digest is
  already in the ring is dropped, a `Block` inside the cooldown window is
  downgraded to a nudge carrying the reason, and a nudge that does not fit
  the queue's count or byte cap is dropped. `Quiet` records nothing at all,
  so a quiet advisor cannot age its own history away. One ring serves both
  channels, and a drained nudge stays in it: repeating advice the primary
  has read is the duplicate the ring exists to stop. Identity is the text
  lowercased with whitespace runs collapsed, SHA-256, truncated to 128
  bits. `Guard` is opaque because two of its four fields are invariants —
  `last_block_review` indexes the same clock `reviews` carries, and a value
  past it would make the elapsed arithmetic negative and shut the block
  channel for the session, which is why `decode` re-checks it. `decode` is
  lenient about an absent field (it takes the empty guard's value) and
  strict about a present one of the wrong type, which is also what carries
  a cell from the build whose clock counted the primary's runs: its `runs`
  and `lastBlockRun` are absent under the names read now, so the clock
  restarts while the ring and the queue survive. `default_policy` is a
  two-**review** cooldown, a ring of thirty-two, and eight nudges or four
  kilobytes pending; only the cooldown is configurable. The clock counts
  reviews rather than runs because one run now holds many: `review_opened`
  is called when a slice is actually handed over, never on one coalesced
  away or one whose send failed.
- `client/advisor.{strand, primary, cursor_key, guard_key, goal_key,
  reevaluate_every_ms, brief, Settings, Wiring,
  Message, ask, start, supervised, hooks, seam, abort_notice,
  ensure_strand, active_tools}` — the actor that joins the two pure
  halves above to the session, and **performs** the session's goal loop
  (protocol 044) whose transitions are `client/goalloop`'s. Its goal code
  is four steps and no decisions: gather what the two strands are doing,
  call `goalloop.next_action`, perform the `Action`, store the cell. The
  loop is level-triggered, so every goal-relevant message — a run end, a
  run start, a review end, an abort, a verdict, a spend trigger, an
  operator command, and `ReevaluateTick` every
  `reevaluate_every_ms` (two minutes, armed once through `weft/actor`'s
  `periodic`; it cannot be armed conditionally because `periodic` is a
  builder field a handler cannot re-time, and the interval is long because
  the tick is only the recovery path and an unconditional tick keeps this
  actor out of hibernation) — is an occasion to read the level rather than an edge to
  apply. The actions it performs are the goal feed (sendable with an
  empty slice, which is what `/goal resume` on an already-reviewed idle
  primary needs), the continuation wake through a door that spends no
  operator-turn budget, and the one-shot wrap-up, which is one-shot
  structurally because the goal is already `Limited` when the action
  arrives and a `Limited` goal rests. It also owns the goal's accounting,
  and there is exactly **one code path that adds**: `PrimarySpent` is a
  trigger carrying nothing, and `account_recompute` scans the usage
  ledger past `accounted_through_seq`, counting a row only when
  `set.contains` says the entry it names is on the primary's branch — the
  branch scan bounded by the accounting window rather than by a row cap,
  because a capped set is a prefix of the chain and would drop the
  primary's older rows as foreign. `ensure_strand` seeds the advisor through
  `api.create_idle_strand` and never through the Agency, so it carries no
  `lineage/` cell: the primary cannot address it, it can address nothing,
  and `strand.roster` does not list it. `hooks` wraps four slots —
  `run_end` casts `PrimaryRunEnded`/`AdvisorRunEnded`, then asks
  `TakeAtRunEnd` and returns whatever it drained as a born-placed
  follow-up when the inner slot placed none (so the primary reads its
  nudges before it stops rather than after the operator next types);
  `usage` casts `PrimaryStepped` for the primary's steps and nobody
  else's; `run_start` asks `TakePending(operation:, reply:)`, which both
  drains the queued nudges with a bounded call and hands back this
  operator turn's one unsolicited-delivery budget if `operation` is not
  the run this actor itself opened; and `context` prepends `brief` to the
  advisor's own requests transiently. `Message` also carries `Judge`,
  which now drains and delivers the queue itself — through the same door
  a block uses — when the verdict is `Queued` or `Downgraded` and the
  primary is idle with its turn unspent, so the decoupled `TakePending`/
  `TakeAtRunEnd` drains are joined by a third, synchronous one inside
  `decide`. The run-end drain also remembers the operation it checked. A
  nudge judged before that operation settles joins its durable follow-up
  queue, so it need not wait for another operator prompt. `seam` is the
  `advise` door, which refuses any caller whose
  durable strand name is not `advisor`. The actor owns `cursor_key` and
  `guard_key`, reads them lazily on its first message because the runtime
  it borrows may not be up at start. Its `Memory` carries two fields
  beyond the durable cells, both deliberately heap-only: `Turn`
  (`Unspent`/`Spent`) is this operator turn's one unsolicited delivery,
  and `woke: Option(OpId)` is the newest run this actor opened on the
  primary, read together at every run start so a run this actor itself
  woke cannot renew its own budget — the loop the duplicate ring cannot
  cut, since a paraphrase passes it. A restart forgetting both costs at
  most one extra wake. **Nothing the goal loop decides from is heap
  state**: the phase, the three bound counters and the accounting pair all
  live in the cell, because an earlier draft kept the phase in the heap and
  every way of losing it left an Active goal with nothing running.
  **The step trigger is what makes a long run legible.** A run is many
  steps and nothing bounds how many, so with the run end as the only
  occasion an agentic loop could work indefinitely unreviewed.
  `Settings.feed_every_steps` (20, or 0 for the run-end-only cadence) is
  the threshold, and it is a **floor rather than an interval**: a feed owed
  while the advisor is still reading is coalesced and owed, and the review
  end pays it, so a continuously working primary is reviewed once per
  review duration. That is what bounds staleness when the reviewer is the
  slower model, and it is also what the pairing costs. The trigger rides
  `usage` because that slot fires once per committed step, returns `Nil`,
  and lands *after* the commit — `admission` is per-step too and wrong
  twice: a decision on the critical path, and fired before the request.
- `client/advisor_pending.{Error, read}` — the one read of the advisor's
  undelivered nudge queue that does not go through the actor. `read`
  takes one exact-key `fact.custom` selection for `advisor.guard_key`
  through the bounded snapshot reader and decodes it with
  `advisorguard`'s own decoder, never with `take_pending`: the drain
  belongs to the primary's run-start and run-end hooks, and a read that
  took the queue would deliver the advice to nobody. A missing cell is an
  empty board — the positive claim that a session's advisor has nothing
  waiting — but a cell present and rejected by the decoder is `Malformed`
  rather than an empty board, because an unreadable cell is not evidence
  for that claim. Backs the `advisor_pending` command; see the wire
  section below.
- `client/memory.{max_sidecar_bytes, digest_reader}` — the two halves of
- `client/goalcommand.{Seam, seam}` — the gateway's six goal commands
  over the advisor actor's registered address: one record of monitored
  bounded calls (`set`, `set_check`, `clear`, `pause`, `resume`) built from the
  advisor wiring, so the gateway holds an option of the seam rather
  than a dependency on the actor's internals. The two call layers
  flatten — an unavailable actor and the actor's own refusal are both
  the operator's answer — because there is no case where a dead plane
  should read as a committed command.
- `client/goal_pending.{Error, read}` — the goal's one read that does
  not go through the actor: an exact-key `fact.custom` selection for
  `advisor.goal_key` through the bounded snapshot reader, decoded with
  `goalstate`'s own decoder. A missing cell is a board whose `status` is
  `none` — no goal is a real state — and a cell the decoder rejects is
  `Malformed` and answers `unavailable`, never a positive empty. The
  `goal_get` command and every committed mutation's fresh-board reply
  project this read; the actor keeps the writes.
- `client/goalstate.{PauseCause, LimitCause, Status, Phase, Goal, new,
  status_of, tokens_used_of, reviewer_note_of, encode_status,
  encode_reason, decode_status, encode_phase, decode_phase, encode,
  decode}` — the session goal's durable shape as pure data, and the
  cell's only codec. The `goal/state` field names are frozen by protocol
  044 §1: `objective`, `status`, `reason`, `phase`, `token_budget`,
  `tokens_used`, `accounted_through_seq`, `cost_used`, `continuations`,
  `zero_progress`, `unanswered_feeds`, `created_ms`, `updated_ms`,
  `reviewer_note`, `check`, `last_check`.

  Two shapes here carry the design. `Status` is `Active | Paused(by:
  PauseCause) | Limited(by: LimitCause) | Complete`, and the cause is
  carried because the status word alone named four different pauses
  (`operator`, `aborted`, `zero_progress`, `reviewer_unresponsive`) and
  two different limits (`token_budget`, `continuation_cap`), which told
  the operator the harness was holding their goal and refused to say why.
  The four wire words do not move; `reason` rides beside them, and the
  decoder **checks the pairing** rather than defaulting it — a `paused`
  with no cause, or an `active` with one, is an error naming the field.
  `Phase` is `Idle | Checking(deadline_ms: Int) | ReadyToFeed |
  AwaitingVerdict(feed: OpId) | Continuing(woken: OpId, since_seq: Int)`,
  stored as `{state, operation, since_seq, deadline_ms}` with each rider null
  outside the state that names it, and it is what makes the loop recoverable:
  a restart reads it and knows a verdict is owed and by which advisor run,
  which primary run is the loop's own, and whether a check is in flight and
  until when. `Checking`'s deadline is durable because the check runs in a
  task linked to the actor, so an actor that dies takes it with it and a
  deadline already passed is the repair rather than an error. Neither
  `Checking` nor `ReadyToFeed` survives a primary that goes back to work:
  both describe the tree as it stood before that run, so the loop returns the
  phase to `Idle` and the check runs again for the stretch the feed is
  actually about.

  `CheckEnding` is `Exited(status: Int) | DidNotFinish(reason: String)` and
  `CheckResult` carries the command that actually ran beside it, so an
  operator who changed the check does not see the old run labelled with the
  new command. The pair is stored as `status`/`not_finished` with exactly one
  non-null, refused rather than resolved when both or neither is present.

  The decoder is strict where the guard's is lenient, deliberately: this
  cell is written whole by one owner, so an absent required field
  (`objective`, `status`, `phase`, `token_budget`, `created_ms`,
  `updated_ms`) is a writer/decoder disagreement rather than an older
  writer. The five counters default to zero when absent and are refused
  when negative — each counts something that only grows — `token_budget`
  is required and positive (v1 has no unbounded goals), and an
  `updated_ms` before `created_ms` is refused. **A cell written by the
  feature's own earlier implementation does not decode**, and that is a
  deliberate clean break: `phase` is required and the status pairing is
  checked, there are no deployed goal cells, and tolerating the old shape
  would mean accepting a goal with no recorded phase — the state the
  rework exists to remove. No transitions live here; those are
  `client/goalloop`'s, and the `goal_set` handler owns the objective's
  4,000-character bound, which is why `new` is total and unvalidated.
- `client/goalcheck.{output_tail_chars, output_bytes, settle_grace_ms,
  slot_wait_ms, slot_retry_ms, slot_never_came_free, step_id, Wiring, Runner,
  wiring, unavailable}` — the operator's goal check
  as one jailed process. It clears through `tools/tool.broker_runner`, the
  closure the `bash` tool itself clears through, under the session's own base
  policy: operator-authored is not exempt from Rule Zero, which is about
  where code runs rather than who wrote it, and a check is deliberately not
  narrowed to a read-only workspace because the commands an operator pins
  write build output. `Wiring` is one blocking closure, the wall it runs
  under and the backstop the caller kills its task at, so an advisor test
  stubs a check that passes, fails, hangs or dies without a broker, a helper
  pool or a jail; `Runner` is the fields the production path needs, which are
  `client/jobs.Wiring`'s own. The backstop is the wall plus the clearance a
  congested helper pool may cost plus `slot_wait_ms` plus `settle_grace_ms`,
  never the wall
  itself: both clocks start in the caller and the task's starts first, so a
  task killed at the wall could never deliver the settlement the sandbox
  reached there. The step id is its own name so the pooled execution budget
  is not shared with the hooks' — and because that pair's ledger admits one
  call at a time, an abandoned check is *cancelled* rather than left running:
  `client/advisor` keeps the witnessed handle and cancels it whenever the
  goal leaves `Checking`, which is what returns the slot through the broker
  relay's caller-watch.
  **The cancel does not return the slot synchronously**, and that is the one
  refusal this module waits out rather than reports: the relay cancels the
  execution and drains it to the helper's terminal event before releasing the
  budget, while the actor starts the replacement at once, so the replacement is
  refused with `OutstandingCapReached` for a reason that says nothing about the
  command. Reporting it showed the reviewer "no exit status" for a command that
  exits immediately, and not only once — the shape recurs every time the primary
  goes back to work with a check in flight, so a whole run's feeds can carry it.
  The wait is `slot_wait_ms` at `slot_retry_ms`, spent in the check's own weft
  task so the actor still never blocks; a slot that outlasts it is recorded as
  `slot_never_came_free`, so a check that can never start still lands a result
  and still feeds. Every other refusal is a decision more time cannot change
  and is reported at once.
  A settlement the wall stopped reports **no** exit status, because a number
  invented for it would read to the reviewer as the command's verdict on the
  work.
- `client/goalloop.{continuation_cap, zero_progress_limit,
  unanswered_feed_limit, check_timeout_ms, Progress, Answer, Event, Observed,
  Action, next_action, defers_the_refeed, feed_opened, primary_woken,
  progress_of, check_did_not_finish, wrap_up_text, stopped_because}` — the
  goal loop's transitions, pure. `next_action(goal,
  observed) -> #(Goal, Action)` takes the goal as stored plus a record of
  fetched facts (each strand's open operation, whether the stretch since
  the feed cursor did work, the occasion, the clock, the wall a check runs
  under) and answers `Rest`, `RunCheck(command)`, `FeedReviewer`,
  `WakePrimary(text)` or `WrapUp(text)`. It is a module
  rather than a set of actor arms because the state space is the product
  of four statuses, five phases, three counters and two strands'
  liveness, and the failure that mattered was a combination nobody had
  enumerated: an Active goal, an idle primary, an idle reviewer and
  nothing to do. `goalloop_test` property-walks that space, and the
  headline invariant is that the combination never answers `Rest`.

  The three bounds live here with their constants. `continuation_cap` (8)
  counts continuations **since the last primary run start the loop did not
  open** — the reset is the bound, since without it the cap was a lifetime
  cap and a resume after a trip tripped again at once. `zero_progress_limit`
  (2) counts woken runs that committed no work, and `progress_of` is the
  predicate: a tool result, or a user entry that is **not** one of the
  harness's own frames, which it recognizes through `advisorslice`'s
  two-token pairs. Counting any user message as progress is what made the
  bound unfireable, because the continuation frame is a user message
  committed after the cursor advances. `unanswered_feed_limit` (3) bounds
  the re-feed of a goal feed whose reviewer run ended without a verdict,
  so a provider that will never answer the goal words pauses the goal
  instead of being paid to refuse forever — and the re-offer is the periodic
  tick's rather than that run end's (`defers_the_refeed` names the one
  occasion the loop deliberately answers with `Rest`), because offering again
  at the run end spent all three tries inside one rate-limit window.
  `check_timeout_ms` (five minutes) is the fourth bound and the only one a
  process rather than a count: it is both the jailed command's own wall and
  the durable `Checking` deadline, so a restarted actor cannot wait on a
  process the sandbox has already killed. The task that runs the command is
  killed later than that, by `goalcheck.Wiring`'s backstop, so the wall's own
  settlement has room to arrive. `wrap_up_text` and
  `stopped_because` are the per-cause wordings, rendered once so the
  primary's wrap-up, the operator's panel and the reviewer's refusals
  cannot word one status three ways.

  bounding the sidecar read, which the lifecycle producer moved onto the
  strand driver's hot path: `max_sidecar_bytes` (four times the render
  cap) is asked of the *file* by one `stat` before a byte is read, so a
  file that could not have come from `render_digest` costs nothing and is
  refused whole rather than clipped to a plausible prefix, while a file
  merely over the render cap is still read and clipped as before.
  `digest_reader` is the reader a server installs — the one that carries
  a logger, so a refusal says `memory.digest_oversize` instead of looking
  exactly like a repository that has never distilled. `read_digest` stays
  the silent door.
- `client/memory.{memory_file, digest_file, store_beside, digest_beside,
  directory_of, fact_type, lesson_type, preference_type, pipeline_types,
  type_named, short_name, max_digest_bytes, max_distillates,
  max_row_chars, lease_ttl_ms, run_lease_ttl_ms, head_key, cursor_key,
  notes_cursor_key,
  note_count_key, Cursor, cursor_from, cursor_value, MemoryFault, Opened,
  open, close, probe, SourceRef, Provenance, no_provenance, Distillate,
  distillate_of, provenance_of, provenance_by_id, names_source,
  append_distillates, head, head_rows, advance_head, replace_head,
  advance_cursors, NotesRewind, Rewind, no_rewind, cursor_rewind,
  cell, notes_after, safe_text, remember_seam, fence, render_digest,
  read_digest, write_digest, reconcile_digest, digest_hooks, wrapped}` —
  the memory plane: the
  `loom-memory.db` session beside the session files (the fold
  `client/history` established, deliberately *not* the workspace), the
  `loom-memory.digest` sidecar the server injects from, and the host side
  of the `remember` door. Opened with the ordinary writer lease and
  `session.ensure_id` called by hand, because nothing here goes through
  `runtime/api` — which is also why its bookkeeping cells need no
  reserved prefix. **The TTL is the caller's**, and the two callers are
  nothing alike: a `remember` write covers one commit, while a run's
  commits are separated by whole provider turns and it takes
  `run_lease_ttl_ms` so no opener can steal the file out from under a
  model turn (the `rewrite_lease_ttl_ms` precedent, and a constant rather
  than a renewal timer because a command has no supervision tree to hang
  one on). **`append_distillates` and `advance_head` are separate
  on purpose**: rows commit first, the head-and-cursors CAS commits last,
  and a run killed between them leaves the previous head, cursors and
  sidecar standing over inert orphan rows. `render_digest` writes the
  body only — scrubbed, byte-capped, truncation marked — and `wrapped`
  builds the fence and the attribution at injection time, so the file
  cannot forge its own provenance.
  The **erasure cascade's** reading surface is here too (#115).
  `provenance_of` is the total decoder of the `{text, sources, derived_from}`
  payload — anything it cannot read is `no_provenance`, which names
  nothing and is therefore *kept*, so one malformed row can never empty
  memory. `provenance_by_id` pairs each head id with what its row
  records, **one pair per id including ids the store cannot resolve**, so
  an unresolvable head id survives the rewrite rather than vanishing from
  it. `names_source` is the match, at session grain because provenance is
  batch-level. `replace_head` is the write: the head cell and the cursor
  rewind in one CAS, no rows, sharing `advance_head`'s expectation so the
  head only ever moves under a CAS. `cursor_rewind` computes that rewind
  (#124) — every `distill/cursor/*` cell wound back to seq zero under the
  generation it recorded, plus the notes cursor when a consolidation has
  consumed anything. Those cells *are* the pipeline's record of what it
  has read, so the prefix scan sees sources a directory walk at cascade
  time could not: a file a live server holds the lease on, a file since
  moved away. `no_rewind` is the empty one, which is what an ordinary
  head replacement passes.
- `client/distill.{Answer, Distiller, Candidate, Extract, Harvest, Report,
  Cascade, CascadeMode, Config, default_scan_limit, max_extract_chars,
  max_notes_per_run,
  default_timeout_ms, distill_owner, extractable, extraction_input,
  extraction_prompt, consolidation_prompt, parse_candidates, config_for,
  with_logger, run, cascade, no_distiller, source_files, target,
  gateway_distiller, capped_gateway_distiller, main}` — the
  distillation pipeline, runnable as `gleam run -m client/distill --
  --config loom.toml [--session-dir <dir> | --session <path.db>]`. It
  walks the session directory's `*.db` files, skips every one whose
  writer lease is held (**that is the live-session rule, entire**), reads
  each above a `{seq, rewrite generation}` cursor, extracts, consolidates
  against the current head and the `remember` notes, and re-renders the
  sidecar. `extractable` is the anti-feedback exclusion and is a rule
  about types: settled assistant text (`client/rules.scannable_text`,
  shared with the rule scanner) plus compaction and branch summaries, and
  nothing from a user turn or a `CustomEntry`. `Distiller` is the whole
  provider surface, so a test scripts both model turns; `target` picks the
  `Summarize` route when the catalogue has one and the resolved main
  identity when it does not, the summary path's own arrangement.
  Two properties of the pass shape are load-bearing rather than
  incidental. **Only the sources extraction got an answer for** drive the
  cursors and the provenance: advancing a failed source's cursor would
  lose its entries permanently and silently, and naming it in provenance
  would over-claim a contribution #115's cascade would act on. And the
  consolidation turn is dispatched on what extraction **produced**, not
  on what it was offered — a repository whose sources all honestly answer
  `nothing` commits a cursors-only transaction, leaves the head alone,
  and does not re-pay the same extraction turns on every run forever.
  Every run, that one included, ends by reconciling the sidecar against
  the head.
  **`--cascade <source-session-id>` is the second command behind the same
  entry point** (`cascade`, issue #115): the first-order erasure cascade,
  run by the operator *after* `session/repo` has rewritten that source.
  It drops from the head every distillate whose provenance names the
  session and re-renders the sidecar without them. Five things about it
  are load-bearing. It **needs no `--config`** and runs under
  `no_distiller`, because it dispatches no model turn. It **writes no
  rows** — every survivor is already durable — so the pipeline's write
  order holds trivially, and it takes the *short* lease rather than
  `run_lease_ttl_ms`, since nothing slow sits between its commits. A
  cascade that drops rows **rewinds the cursors in the same CAS**
  (#124): a head is uniform in provenance, so an effective cascade
  empties it, and re-reading the surviving sources is the only rebuild
  there is, so `memory.cursor_rewind`'s cells ride inside
  `memory.replace_head`'s transaction. One transaction is the point — a
  crash between an emptied head and a separate cursor write would leave
  the head empty above high-water cursors, which is the unrecoverable
  state the issue described. The next ordinary pass re-extracts every
  readable source and folds the notes in again, which costs one
  extraction request per source plus one consolidation and is the only
  rebuild on offer; `distill_test`'s
  `an_emptying_cascade_rewinds_so_the_next_pass_rebuilds` drives the
  whole loop. **`CascadeMode` is the fifth**: `Preview` (`--dry-run`)
  computes the same answer — dropped, kept, rewound, notes — reports it
  and writes nothing at all, no CAS and no sidecar, so an operator sees
  the wipe and its price before paying either. And a cascade that
  drops nothing **does not CAS at all**, because writing the identical id
  list back would still bump the cell's seq and lose a concurrent run's
  expectation — so it rewinds nothing either. First-order: a row whose `derived_from` names a dropped
  row is not chased, and within one head that is vacuous rather than
  deferred. The induction is written out at `memory.replace_head` and
  needs **both** head writers: `advance_head` mints a fresh batch whose
  ids are disjoint from what its `derived_from` names, and `replace_head`
  writes a *subset* of an existing head, which preserves that
  disjointness. A third head writer introducing new ids would void it
  silently. The `Cascade` report carries an `unreadable` count — rows
  kept because their provenance would not decode, which is the one place
  the command under-deletes. `docs/spec-gaps.md`'s M2
  item 9 carries the boundary and the over-deletion it implies.
- `client/glance.{Wiring, Report, Message, request_timeout_ms,
  max_answer_tokens, request_deadline_ms, branch_window, target,
  summarizer, hooks, context_of, is_watched, start, supervised}` — the
  glance loop, one **`weft/state_machine`** per session that writes a
  `core/glance` cell under `client/glance/{strand}` for every running
  strand but `main` and `advisor`. `hooks(built, name)` wraps the `usage`
  slot with a cast of `Stepped(operation, context)` and captures only the
  name and the inner slot, because the hooks are copied into every strand
  driver. The machine resolves the strand through `notes.strand_of`, asks
  `client/glancepace.step` what to launch, and arms one named timeout
  (`glance-wake`) for the plan's `wake` or cancels it on `None`, so an
  idle session holds no timer. It has one state, `Watching`, the
  `schedulescan` arrangement. Each launch is a one-task `weft` run under
  `request_deadline_ms`, relayed into a sink subject created for it. The
  sink is selected (`sm.with_selector`, rebuilt on every plan) exactly
  while its flight is booked, and a flight clears only on the run's last
  word (`AllDelivered` or `RunLost`), never on the outcome alone, so a
  strand is not launched again while its worker is alive and a relay's
  trailing message never arrives unselected. The task does all the I/O.
  It returns `Gone` when the operation is no longer the strand's live one.
  Otherwise it reads `op.meta`, the prompt entries and at most
  `branch_window` branch entries back to the source leaf straight off the
  store, reuses the stored title when the cell names this operation, asks
  the summarizer, and writes with `api.put_reserved_fact` through the
  borrowed runtime. `target` is `summarize`, then `subagent`, then `main`,
  as `ForRole(role, Some(ThinkingOff))`; `summarizer` caps the answer at
  `max_answer_tokens` through `distill.capped_gateway_distiller`.
  `context_of` is `input + cache_read + cache_write + output` and `None`
  for an adjustment row. The glance's `tokens` is that figure from the
  newest row, never a sum. A failure is logged as `glance.unusable`, at
  warning level on the first failure of a streak and at debug level
  after. Nothing is written on failure, and the cell is never deleted.
  `client/serve` starts it in the restartable service tier only when
  `summarizer` resolves (otherwise it logs `glance.unavailable`). There
  is no config switch.
- `client/glancepace.{Pace, default_pace, Activity, Phase, Track, Book,
  Ending, Event, Launch, Plan, new, track, step}` — the glance loop's
  schedule as one pure function, `step(book, event, now, pace) -> Plan`.
  A strand's first `Stepped` in an operation is due at once. After
  `Summarized`, the next request is due `every_ms` after the previous
  request *started*. Only a `Fresh` strand, one that stepped since its
  last request began, is ever launched. `Asking` strands are never
  relaunched, and the launch pass fills at most `concurrency` slots, most
  overdue first with ties broken by strand name. `Unusable` sets the
  strand `Fresh` again and backs it off from `retry_ms`, doubling to
  `retry_cap_ms`. `Ended` forgets it, and a `Resting` strand quiet for
  `retire_after_ms` is forgotten lazily on the next step. A successor
  operation inherits its predecessor's `Asking` phase, so the strand's
  one slot stays held until the old answer settles. `Plan.wake` is the
  soonest due instant of a resting, fresh strand. A due strand that found
  no slot needs no wake, because a slot frees only on a settlement, which
  is an event.
- `client/glanceslice.{max_prompt_bytes, max_calls, max_call_bytes,
  max_said_bytes, Title, Material, Reply, gather, request, parse}` — what
  the glance summarizer is shown and how its answer is read, pure.
  `gather` takes the prompt entries in acceptance order and the branch
  newest first. It keeps the accepted user text (1500 B), the newest
  eight tool calls oldest first (200 B each, `name <json args>`) and the
  newest visible assistant text (500 B, never thinking), each through
  `core/glance.clip`. `request` fences the material as data and asks for
  `TITLE:` and `NOW:` when `Untitled`, `NOW:` alone when `Titled`.
  `parse` is total: labels match case-insensitively through markdown
  bullets, bold and quoting, and the first non-empty line per label
  wins. Against `Titled` an answer without a `NOW:` label may stand on
  its first unlabelled line, provided that line starts with a word ending
  in "ing", since models asked for one line often drop the label. A
  missing or empty line is `Error(Nil)`, and values are clipped to
  `glance.max_title_bytes` and `max_summary_bytes`.
- `client/distillpass.{Cadence, Options, Pass, Config, Message,
  default_wall_ms, default_options, no_pass, parse, start, supervised,
  settled,
  started_event, completed_event, failed_event, expired_event,
  off_event}` — the distillation pass in the *session lifecycle* (#149):
  the supervised worker that makes `client/distill` run without a source
  checkout, a cron job or an operator. A `weft/state_machine` with two
  states — `Running` while the pass is in flight, `Idle(pass)` for the
  rest of the boot — started under the service tier by
  `serve.with_distill_pass`, which is *after* the session's own writer
  lease is held, so the live session is skipped by the pipeline's
  existing lease rule rather than by a new one. The pass itself runs on
  a `weft` scope bounded by `Options.wall_ms` and relayed back onto the
  machine's own subject, so the machine stays able to answer while it
  runs; `settled` is that answer, and a question asked mid-pass is
  `postpone`d and replayed on the transition rather than polled for. It
  **never re-arms**: one pass per boot is the whole cadence, and the whole
  retry policy is "the next boot reads the same material again", which is
  safe because the pipeline moves no cursor until it succeeds. `parse` is
  the total decoder for the `[memory]` table (`distill = "on-boot" |
  "off"`, `distill_wall_ms`), whose key must also be in `client/catalog`'s
  allowed top-level list — the obligation `[[rule]]` and `[schedules]`
  carry. `default_wall_ms` **is** `memory.run_lease_ttl_ms` and is also
  the ceiling: nothing renews the memory lease but a commit, so a pass
  cannot usefully outlive it, and a larger configured value is refused
  rather than clamped. `Cadence` is a two-variant type rather than a flag because the
  opt-out is a posture an operator takes deliberately.
- `client/scratch.{Bounds, Message, Scratch, start, supervised, stop, seam,
  none, stat, default_bounds}` — the ephemeral scratch store `cap/kv`
  reads and writes: a session-scoped actor, addressed by process name the
  way the Agency is, holding string keys to bytes. Three bounds, each on
  a different way to grow: 256 KiB per entry (refused in band), 8 MiB
  total and 1024 entries (both **evicted**, least-recently-written, which
  is what `cap/kv`'s "may be evicted or reset between calls" contract
  asks for). `Bounds` is a plain record, so `start` passes every one
  through `coherent` first — each field floored at 1, the per-entry
  bound never looser than the total — because a `max_entries` of 0 makes
  the store evict everything it is handed, which a program reads as
  unrelenting eviction and loops on rather than failing. Clamped rather
  than refused: this is a cache whose contract already says values may
  vanish. Gone on restart, deliberately — a store a program can write
  across sessions is a channel from one execution's model output into a
  later execution's input. `none()` is the seam a host with no store
  hands out, and it *refuses* rather than silently succeeding: a `set`
  answering `Ok` into nothing reads to a program as an eviction and it
  loops.
- `client/rules.{Rule, parse, fires_on, scannable_text, injection,
  fired_key, cursor_key, fired_value, cursor_value, cursor_seq,
  max_rules, max_triggers, max_name_length, max_trigger_length,
  max_body_length}` — the triggered project rule store (issue #27,
  design §8). A rule is `{name, triggers, body}` read from `[[rule]]`
  tables in the same `loom.toml` the catalogue comes from, parsed by the
  same strict, total discipline: unknown keys refused, empty and
  oversized fields refused, duplicate names refused, all in words an
  operator can act on. Rules are **operator configuration and never
  model-writable** — a rule is text the harness injects into a model's
  own context, so a store a model could write would be durable prompt
  injection with a delivery mechanism attached. Triggers are literal,
  case-sensitive substrings, any-of per rule, matched over an assistant
  entry's visible text through `events/search.entry_text` — no regex,
  because an operator-supplied pattern reached from model output is an
  unbounded computation driven by the thing being matched. `injection`
  is the fenced, attributed text a fire actually commits.
- `client/rulescan.{Options, default_options, with_logger, start,
  supervised, default_scan_limit, default_checkpoint_every}` — the
  session-scoped scanner actor. Its mailbox *is* `runtime/writer.Event`,
  so it is a routed writer subscriber beside the commit
  forwarder; on each hint it re-reads the store above a durable cursor
  and never trusts the hint for anything. A fire is one
  `api.steer_marking` — the injection and the rule's write-once
  fired-mark in one transaction — so injected-never-twice is a property
  of the commit rather than of the actor's memory (at-most-once in
  full: the abort corner is `docs/spec-gaps.md`'s item 8). A rule that trips on an
  **idle** strand is *held*, not dropped and not started: the cursor
  stays frozen so the next run finds it again. A held strand the
  `runtime/lineage` ledger says was reaped is judged provably dead
  (issue #113) and **abandoned** rather than retried forever: the
  in-memory `Progress` gets a third state beside held-and-not, the
  pending rules stay unfired (nothing lies about what happened), and the
  transition is logged once, `rule.hold_abandoned`. A terminal
  `strand.last_result` alone is deliberately *not* this signal — see the
  next invariant — so the ledger read happens once per strand per
  incarnation, on the pass a hold begins, and costs nothing before or
  after.
- `client/schedule.{Schedule, Owner, Timing, Expiry, Wake, Lateness,
  Origin, Policy,
  parse, parse_policy,
  default_policy, policy_opens_the_door, wake_under, build,
  encode, decode, config_key, config_key_prefix, strand_prefixes,
  parse_instant, render_instant, parse_utc_offset, render_utc_offset,
  max_utc_offset_s, min_utc_offset_s, fired_key,
  fired_key_prefix, fired_value, seen_key, seen_value, decode_seen,
  injection, origin_of, interval_occurrence,
  interval_late, recurring_expired, cron_occurrence, cron_late,
  cron_next_delay_ms, relative_instant, max_schedules, max_model_schedules,
  min_interval_s,
  max_name_length, max_body_length, max_target_length,
  default_max_fires, max_max_fires, default_expires_after_s,
  max_expires_after_s, min_in_seconds, max_in_seconds}` — scheduled heartbeats (`docs/design-notes/
  scheduled-heartbeats.md`), the time-triggered sibling of `client/rules`:
  a `[[schedule]]` fires on a clock instead of on a literal match, parsed
  from the same `loom.toml` by the same strict, total discipline. A
  schedule reaches the scanner from one of **two** stores and is the same
  value either way: the operator's `[[schedule]]` tables, and the model's
  own `schedule/config/…` reserved cells created through
  `tools/schedule` (`encode`/`decode`/`config_key`). `Policy` is the
  operator's say over that second door — `off`/`steer`/`wake` from a
  `[schedules]` table — and `default_policy` is **`ModelSchedulesSteer`**: the
  tools are registered and a model may create schedules, but none of
  them can wake an idle strand until the operator writes
  `[schedules] model_created = "wake"`. `off` disables model creation;
  `steer` injects only into an open run; `wake` also permits fresh runs on
  idle strands when the schedule requests waking. Schedules onto subagents
  always steer. Every recurring schedule carries `expires_after_s`,
  defaulting to and capped at 604800 seconds (seven days), measured from
  the scanner's first durable observation. One-shots fire at most once and
  may fire after their due time. Expiry bounds one schedule, not provider
  spending: a model can
  create another, so unattended check-ins require an operator opt-in. The
  design note's addendum has the whole history — the feature shipped
  operator-only, reopened with an open default on the strength of the
  per-schedule expiry, and settled on `steer` once #161 showed that
  expiry bounds a schedule and not a session (a fresh name is a fresh
  clock), which under the priority order makes unsupervised liveness an
  opt-in. `build` is the constructor the model-facing door and `decode`
  share: it enforces exactly what `parse` enforces, through the same
  predicates and constants, so the two creation paths word refusals
  differently and can never disagree about what is allowed. A schedule is
  exactly one of three: `Interval(seconds, expiry)`, aligned to a fixed
  grid (`slot = floor(now_s / seconds)`); `Cron(expression, offset_s,
  expiry)`, a five-field calendar expression parsed by `client/cron` and
  read against a UTC clock shifted by `offset_s`; or
  `OneShot(at)`, a single RFC3339 UTC instant. Never two, never none, and
  still **no timezones anywhere** — the design note cut cron as a swamp,
  and what changed is that the swamp is the *timezone* half rather than
  the syntax half.
  **`offset_s` is a fixed offset and emphatically not a zone**, which is
  the whole of what the addition costs and what every description a model
  or an operator reads has to say. A zone is a function from an instant
  to an offset — Berlin is +01:00 in January and +02:00 in July — and
  answering which needs the IANA database this tree will not carry, so a
  schedule written `+02:00` in summer fires an hour off its author's own
  clock all winter. What it buys is the thing an operator actually asks
  for: "09:00 my time", which a UTC-read expression cannot say at all.
  Written `utc_offset = "+02:00"` in TOML (`[+-]HH:MM`, allowed **only**
  beside `cron` and refused in words beside `every`/`at`), carried as
  `utc_offset_s: Int` in the config cell (absent → 0, so every cell
  written before the offset existed decodes unchanged), and parsed by the
  one `parse_utc_offset` all three doors share. The bound is
  `min_utc_offset_s`..`max_utc_offset_s`, ±14 hours, held by
  `parse_utc_offset` *and* by `build`, because `decode` reaches the
  constructor with a number that never passed through the parser.
  **The shift is a reading of the clock, never a change to an
  occurrence's identity**: an occurrence at UTC epoch `t` matches when
  `cron.matches(expression, at_s: t + offset_s)`, and the three search
  functions shift in and shift straight back out — so every occurrence id
  they return is a **UTC** epoch second and `fired_key`, `seen_key` and
  `config_key` are byte-identical to what they were. Storing ids in
  shifted time instead would have made an edited offset unread the whole
  fire history and re-fire everything. `cron` buys the one thing an interval cannot say at all:
  a **phase**. The interval grid is aligned to the epoch, so `every =
  "86400s"` is always 00:00 UTC and `0 9 * * 1-5` is the only way to ask
  for 09:00 on weekdays. It needs no `min_interval_s` check, and that is
  a property of the grammar rather than an omission — cron's finest grain
  is a minute, so `* * * * *` is already exactly at the floor.
  **`Interval` and `Cron` answer "what is due" differently, on purpose.**
  An interval fires the slot `now_s` falls in whether or not that slot
  began before the schedule existed, because a heartbeat grid has no
  wall-clock meaning and "the current slot" is the only honest reading of
  *now*. A cron occurrence *is* an instant somebody named, so
  `cron_occurrence` answers with the last match at or before `now_s`
  (`cron.previous_occurrence(before_s: now_s + 1)`) **and only if that
  match is at or after `since_s`** — the observation instant from the
  seen cell. A `0 9 * * *` schedule created at 15:00 therefore does not
  fire this morning's 09:00; it waits for tomorrow's. `cron_late` follows
  the same instant: the preceding occurrence has to have been *owed*
  (`>= since_s`) and unmarked for a fire to be `Late`, which is what
  makes a first fire on time. `cron_next_delay_ms` is the re-arm, floored
  at a second like the interval one, and `None` — no match within
  `cron.search_horizon_days` — is a schedule that will not fire again.
  All three take `offset_s`, search in shifted time and answer in UTC;
  `cron_next_delay_ms` in particular shifts the match back *before* it
  meets `now_ms`, since shifting only one of the two would arm a delay
  wrong by the whole offset.
  Both recurring shapes carry a
  mandatory `Expiry`: `max_fires` and `expires_after_s` are both always
  active, defaulted when unset, and whichever is reached first ends the
  schedule — the guardrail that caps one schedule's durable fire-mark
  footprint at exactly 1,000 rows and makes `WakesIdle` (below) safe to
  offer at all. The two bounds read two different durable facts.
  `max_fires` counts the fired-marks; `expires_after_s` is measured from
  `since_s`, **the instant the scanner first observed the schedule**,
  which it records once in the cell `seen_key` names (`seen_value`,
  `decode_seen`) — a third corner of `schedule/`, disjoint from the
  `fired/` marks and the `config/` cells by its second segment, and
  unreachable by `put_fact` like both. Measuring age from the earliest
  fired-mark, which is how this shipped, gave a schedule that never
  landed a fire no clock at all: a steer-only heartbeat on a strand
  nobody opens a run on ticked for the life of the session while
  `expires_after_s` read to an operator as a week (issue #157). The
  cancellation tombstone is gone with it — `cancelled_value` no longer
  exists, because cancelling a model-created schedule now deletes its
  config cell (`api.delete_reserved_fact`), which is what keeps
  `config_key_prefix` a list of live schedules. `Wake` is `WakesIdle | SteersOnly` and `Lateness` is
  `OnTime | Late`: both are two-variant types rather than the `Bool`s
  they were, because each is read at one end and written at another and
  neither name carries its own polarity. The TOML `wake` key, the stored
  config cell and the tool result all stay booleans on the wire, and
  each boundary writes that translation down once.
  `relative_instant` (with `min_in_seconds`/`max_in_seconds`) turns a
  model's "in N seconds" into the absolute epoch second a `OneShot`
  needs, refusing anything outside 1..604800 in words. It exists because
  **the model has no clock**: the system prompt carries neither the date
  nor the time, so a model asked to check back in 45 minutes cannot
  compute the instant `at` wants, and the model-facing one-shot was
  unusable in practice for the case it is most wanted in. The caller
  supplies `now_s` rather than this module reading one, because this
  module performs no I/O — `client/scheduleseam` reads the injected
  `runtime/effects.clock`.
  `interval_occurrence`/`interval_late`/`recurring_expired`/
  `cron_occurrence`/`cron_late`/`cron_next_delay_ms`
  are pure functions over the occurrence arithmetic — deliberately
  factored out of the actor that drives them so a fencepost error gets a
  direct, deterministic test rather than one hidden behind a timer
  harness.
  **A schedule has an `Owner` as well as a `target`, and the pair is
  what bounds its life** (#163, #154). `Owner` is `OperatorOwned |
  StrandOwned(strand)`: a `[[schedule]]` table parses to the first, a
  model-created cell always carries the second, and `decode` has **no
  path** to `OperatorOwned` — a cell with no `owner` field reads as
  `StrandOwned(target)`, which is what every cell an earlier build wrote
  actually was, and the absent field can therefore only ever name the
  strand the schedule already fires onto. Ownership decides who may
  `list` and `cancel`; the target decides whose context a fire lands in.
  Keying cancellation on the creator is the fix: a schedule used to be
  cancellable only by the strand it fired onto, so a subagent's own
  heartbeat became uncancellable the moment that subagent settled and
  held a ceiling slot for the rest of the session. What ownership
  deliberately does *not* touch is a schedule's **identity**, which
  stays `{target, name}` — config, seen and fired keys are unchanged and
  name uniqueness is still per target across both stores, because an
  occurrence is a fact about a strand's timeline and two schedules
  sharing that pair would share a mark whoever owned them.
  `strand_prefixes(strand)` is the one place a strand's whole durable
  scheduling footprint is written down — the config, seen and fired
  prefixes, each ending in `/` so a reap cannot reach a
  similarly-named neighbour — and it is what `client/scheduleseam`
  retires on a run end. `Origin` gained a third variant with the owner:
  `OperatorConfigured | SelfScheduled | OwnerScheduled(owner)`, derived
  from the value by `origin_of` rather than paired on by the scanner,
  and the third one exists because text a parent scheduled onto a child
  must not reach the child as "a heartbeat *you* scheduled" — it names
  the owner and says the instruction is worth what a steer from that
  strand is worth, and no more.
- `client/scheduleseam.{Wiring, Door, seam, door, limits,
  describe_timing, cell}` — the host half of the model-facing scheduling
  door, filling `tools/schedule`'s seam the way `client/memory` fills
  `tools/remember`'s and for the same reason (`tools` may not reach a
  session). It is the **only** enforcer of three things: the operator's
  `Policy` (applied through `schedule.wake_under`, which caps `wake`
  rather than vetoing the call, so the tool can tell the model what it
  actually got), the `max_model_schedules`
  ceiling, and the shared bounds via `client/schedule.build`.
  `requested_timing` maps the door's four `RequestedTiming` shapes onto
  one `Timing`, and it is the seam's job rather than the door's because
  everything it needs is on this side: the one RFC3339 parser, the one
  cron grammar (`client/cron.parse`), the defaulted `Expiry` a recurring
  schedule always gets, and — for the relative one-shot `In(seconds)` —
  the session's injected `runtime/effects.clock`, read through
  `core/clock.read` exactly as the scanner reads it, never a wall-clock
  call of its own. `In` is the only arm that reads a clock, and it is the
  reason the argument exists at all: the model is told no date, so it
  cannot compute an `at`. `requested_expiry` defaults `max_fires` and
  `expires_after_s` and checks *neither* — both go on to
  `schedule.build`, whose `checked_expiry` is the same ceiling a
  `[[schedule]]` table meets, so this door holds no second copy of a
  bound. `describe_timing` is the one rendering both `create` and `list`
  use, and a cron reads back as `cron "0 9 * * 1-5" UTC+02:00, at most N
  times` — the clock said out loud, because a caller reading
  `0 9 * * 1-5` has no other way to know which 09:00 it got. A schedule
  with no offset still renders bare `UTC`
  (`schedule.render_utc_offset(0)`), because most have none and spelling
  `UTC+00:00` out every time would train a reader to skip the clause that
  matters on the rare one that does. `requested_timing`'s cron arm parses
  the door's `utc_offset` through `schedule.parse_utc_offset` — the same
  parser the operator's TOML key uses, so a model and an operator cannot
  disagree about `+05:30` — and an absent argument is plain UTC, which is
  what every request meant before the argument existed. `Wiring`
  carries `operator_schedules` for one reason worth knowing: both stores
  feed one scanner, which derives a fired-mark from `{target, name}`
  alone, so a model name colliding with an operator's would make two
  schedules share a mark and suppress each other — and this is the only
  place that check can live. `Wiring.runtime` is a **borrow closure**,
  not a runtime: the registry this seam joins is threaded into
  `api.open`, so it is built before a runtime exists, and it reads the
  session's one holder through `agency.borrow_runtime` rather than
  standing up a second actor. `Door` is the same three operations keyed
  on the **caller's** strand name, which is what `client/codemode` serves
  `schedule.*` over — one implementation behind both doors, so a program
  and a tool call cannot disagree about what this session's schedules
  are. `list(caller)` answers every cell whose *owner* is the caller,
  child-targeting ones included, and `cancel(caller, name, target)` finds
  `{target or caller, name}` and requires the owner to match, else
  `NotFound` — the same answer a name that does not exist gets, so a
  caller guessing at a sibling's name learns nothing from the
  difference.
  Both of its writes are guarded rather than blind: `create` commits the
  config cell with `api.put_reserved_fact_expecting(expected: None)`, so
  a name belongs to whichever writer commits first and the loser is
  answered `NameTaken` rather than having its schedule replaced — which
  is what makes "creating never silently replaces" a property of the
  commit instead of the tool door's `Exclusive` serialization, an
  argument that never covered the code-mode door's per-plan processes
  (#162). `cancel` deletes the cell through `api.delete_reserved_fact`
  instead of writing a tombstone over it, both because every scanner
  tick and every seam call reads the whole `schedule/config/` prefix
  (#164) and because, once `create` commits on the cell's absence, a
  tombstone would hold the name against every later create for the life
  of the session. The pre-checks stay — `name_is_free` is the only thing
  that can catch an operator's `{target, name}`, which has no cell for a
  claim to collide with — and the ceiling stays inexact under a
  code-mode fan-out, which can over-admit by up to `max_outstanding`;
  nothing rests on the exact count, so it is documented rather than
  fixed.
- `client/cron.{Expression, parse, source, matches, next_occurrence,
  previous_occurrence, max_expression_length, search_horizon_days}` —
  standard five-field cron: the parsed expression, what it refuses in
  words, and pure occurrence arithmetic over a UTC calendar. `Expression`
  is opaque because its value sets carry invariants only `parse`
  establishes (sorted, deduplicated, in range, `7` folded to `0` in
  day-of-week), so `client/schedule` stores the **source text** and
  re-parses on `decode` rather than storing an expansion. `parse` is
  total and its error is prose, because the only thing a caller does with
  it is put it in front of whoever wrote the expression. Both searches
  are strictly-greater/strictly-less and bounded by
  `search_horizon_days` — a legal expression need not recur (`0 0 30 2 *`
  asks for the thirtieth of February), so an unbounded search would not
  return. The one rule a reimplementation gets wrong is in here and
  documented at length: **when both day fields are restricted they are
  ORed, not ANDed**, so `0 9 1 * 1` fires on the first of the month *and*
  on every Monday. This module holds **no offset and no timezone
  handling at all**, and that stayed true when `client/schedule` grew a
  fixed `utc_offset`: the shift is applied by the caller, which adds
  `offset_s` to the instant it asks about and subtracts it from the
  answer, so nothing here reads a clock or knows an offset exists. No seconds field, no names, and
  `L`/`W`/`?`/`#` refused by name. Performs no I/O and reads no clock:
  the scanner supplies the instant.
- `client/scheduleadmin.{Admin, Row, CancelRefusal, admin}` — the
  operator's door onto the scheduling store, opposite
  `client/scheduleseam`'s model-facing one: it lists **everything** (an
  operator watching a session watches all of it) and cancels only what a
  strand wrote. A `[[schedule]]` table has no durable cell — it is
  configuration, parsed at boot, and the file is the record, the posture
  `client/rulescan` takes toward `[[rule]]` — so naming one is refused
  with the reason rather than ignored. Cancellation reuses
  `scheduleseam.retire` and then `schedulescan.poke`: one deletion order
  for both doors, because the order (marks, seen cell, config cell last)
  *is* the crash story, and a second order written here would be a
  second chance to get it wrong. `gateway.with_schedules` takes the
  `Admin`, and `client/serve` builds it over the very
  `scheduleseam.Wiring` the model's door uses, so `None` there means no
  admin.
- `client/schedulescan.{Options, ModelDoor, Message, max_timer_delay_ms,
  default_options, with_logger, with_model_door_open, poke, start,
  supervised}` — the
  scheduled-heartbeat scanner, and a **`weft/state_machine`**
  (loom#165). One state, `Watching`, which the machine never leaves; the
  data is the parsed operator list plus the runtime and nothing else; and
  the whole of its liveness is one **named** timeout under a single
  constant name, re-armed by every scan for the soonest boundary any
  still-active schedule needs. A scan that finds nothing active and has
  `DoorShut` `sm.cancel_timeout`s that name instead, so every path
  through a scan says what should be armed under it and the timer belongs
  to the machine rather than to a phase (`docs/weft.md` rule 8). The
  first arming is a zero-delay one made by `sm.on_enter`, which runs
  exactly once because the machine has one state — an injected
  `sm.continuing(Rescan)` would have scanned inside `start`'s own
  continuation, and "the first tick is armed and has not run" is a state
  the fixtures hold still and step through. Neither `weft/actor` nor a
  periodic timeout: the delay is *recomputed per scan* from the store,
  which neither a state timeout (cancelled by a transition this machine
  never makes) nor a fixed cadence can carry. It arms through
  `sm.with_timer_source(timer.Injected(after: runtime.effects.timers.after))`,
  so a simulated session's heartbeats still run on logical time and
  `schedulescan_test`'s fake wheel still drives them by hand — that
  injectable source (weft 0.4.2) is the thing whose absence kept this
  module hand-rolled. With it the generation tag `Message` used to carry
  is gone: `Message` is `Tick | Rescan`, arming under one name supersedes
  the previous arming, and a superseded wake dies in weft's own timer
  book rather than in a guard written here. `start` and `supervised`
  keep `gleam/otp/actor`'s `StartResult` and
  `supervision.ChildSpecification` — weft returns upstream's own types —
  so `client/serve` needed no change at the boundary.
  Every tick unions the operator's fixed
  list with the model's config cells read fresh from the store, never a
  cached list: a cell can appear or be cancelled between any two ticks
  and this actor is restartable, so a cache would be a second source of
  truth. The union is an append and nothing more, because a schedule now
  carries its own `client/schedule.Owner`: whose text a fire is comes
  from `schedule.origin_of` rather than from a pairing this actor used to
  carry, and a config cell can never decode as the operator's.
  `poke` is what the seam rings after a write so a new schedule
  starts on time rather than at the next armed deadline; it checks the
  name is registered first, because a send to an unregistered name
  raises and the caller is a tool body. `with_model_door_open` sets
  `Options.model_door` to `DoorOpen`, which keeps a slow rescan floor
  when the model may create schedules, so a lost poke self-heals within
  one `min_interval_s` instead of stalling forever.
  `process_schedule` dispatches the three timings, and `process_cron`
  mirrors `process_interval` step for step — read the marks
  (`marked_occurrences`, shared), settle the observation instant
  (`observed_since`), test `schedule.recurring_expired`, fire what is
  due, re-arm. The cron-specific arithmetic is all in `client/schedule`,
  so this half stays a scanner. Two differences are worth knowing. The
  seen cell is claimed by this actor as it always was, so `since_s` is
  available on a schedule's very first tick — which is exactly the tick
  that decides whether anything is owed, since a cron occurrence
  predating `since_s` was never asked for. And a cron expression with no
  next match inside `cron.search_horizon_days` is `Expired` for the
  tick, with a `schedule.cron_never_matches` warning: `0 0 30 2 *` is
  legal, will never fire, and must not leave the machine waking up over
  it forever. Unlike
  `client/rulescan` it is driven by its own named timeout on the injected
  `runtime/effects.Timers` clock, never by a writer hint, and it
  holds no progress state across ticks: every tick re-derives which
  schedules are due or expired from a bounded scan of the write-once
  fired-marks already in the store, read straight off `storage`
  (`client/schedule.fired_key_prefix`) rather than through the writer's
  mailbox — the same isolation `client/rulescan`'s direct reads give it,
  for the same reason: a slow tick must never queue in front of a
  settlement. The one fact the marks cannot supply is when a schedule's
  `expires_after_s` window opened — a schedule that never fired has no
  earliest mark — so the scanner is also the **single writer** of that
  schedule's `client/schedule.seen_key` cell, and claiming it is its only
  write besides the fire itself. `observed_since` reads the cell off the
  store like a mark and, finding it absent, claims it with
  `api.put_reserved_fact_expecting(expected: None)`; a `FactConflict`
  means another incarnation won the gap, so the winner's instant is read
  back rather than assumed. The cell is written at most once per
  `{strand, name}` — the invariant every reader of it rests on — and a
  store fault that leaves it unrecorded logs `schedule.seen_unrecorded`
  and measures that one tick from `now`, which expires nothing: a fault
  must neither shorten a schedule's life nor lengthen it. The same "durable-derived beats durable-stored" argument
  keeps `client/rulescan`'s cursor a checkpoint rather than a source of
  truth.
  **A settled target ends the schedule, and the check is a subagent's
  only.** Before any timing arithmetic, a schedule whose target is a
  subagent asks whether that strand has stopped: dead when its lineage
  cell carries the `reaped` mark, or when its `brief` has a terminal
  result (`api.await_strand_result(within_ms: 0)`, one immediate store
  read) — exactly the pair `client/agency.is_live` and `reap` decide
  from. A dead target's schedule is `Expired` for that tick: no fire, no
  re-arm, and no wake whoever configured it, which is the belt to
  `client/scheduleseam.reaping_hooks`' braces (#163). It **fails
  closed**: a `sub:` target with no lineage cell, or one whose cell will
  not decode, reads as finished — the opposite direction from
  `client/rulescan`'s hold, deliberately, because a held rule costs a
  tick while a fired schedule may open a run. A root strand answers
  `False` without a read at all: `main` is idle between runs rather than
  finished, so a terminal result there says the last prompt ended and
  nothing about the next one.
  A fire is one `api.steer_marking` when `Schedule.wake` is `SteersOnly`
  — the injection and the occurrence's write-once fired-mark in one
  transaction, exactly `client/rulescan`'s at-most-once argument, held on
  an idle strand rather than dropped or started. `WakesIdle` opts into
  `api.send_to_strand_marking` instead, which may start a fresh run on an
  idle strand: the one behavior a triggered rule is never allowed, safe
  here only because a schedule's mandatory expiry bounds how long it can
  keep a session alive. A session closed through a missed window catches
  up to **at most one** late fire per schedule at the next tick, never a
  replay of the backlog: an occurrence is judged late by whether the
  *immediately preceding* slot's fired-mark is missing, never by
  comparing the current time against a boundary computed from that same
  current time, which is always trivially on-time by construction.
- `tools/tail.{Tail, Since, new, push, since, received}` — the pure,
  bounded rolling window over one stream with a monotone byte cursor that
  the job runner holds per stream. It lived here as `client/jobtail` until
  the foreground tool collector needed the same primitive
  (`protocol-change/031`); `packages/tools/CLAUDE.md` now documents it.
  What the runner relies on is unchanged: `since(cursor)` tells a reader
  that fell behind how much it missed rather than handing it a
  plausible-looking window, and binary output goes out rather than being
  held back forever waiting for a character boundary.
- `client/retryconf.{default_policy, unbounded_attempts, parse_policy}` —
  the optional `[retry]` table from the same `loom.toml`, parsed by the
  same strict, total discipline as `[schedules]` and `[jobs]`: unknown
  keys refused, `attempts = 0` refused, a negative delay refused, each
  refusal naming the key. It produces a `machine/operation.
  NormalizedRetryPolicy`, which `serve.Settings.retry_policy` carries to
  `api.Options.retry_policy`, so an operator's ladder is the one every
  run on the session uses. `attempts` is a positive integer or the
  string `"unbounded"`; `base_delay_ms` and `max_delay_ms` are
  non-negative milliseconds. A missing table, and every key a present
  table omits, comes from `runtime/api.default_retry_policy` — the one
  place those three numbers live, so the parser cannot drift from the
  runtime default. That default never gives up, so the table is mostly
  for the operator who wants a bounded ladder that fails the run instead
  of long-polling a provider that is refusing.
- `client/notice.{Work, Delivered, Activity, Due, IdleClock, key_prefix,
  heartbeat_tick_ms, default_heartbeat_ms, key, deliver, activity, beat,
  idle_clock, observe, retain, heartbeat_text, minutes}` — harness
  notices about work that outlives its tool call. `deliver` is the
  completion notice a finished job or async execution sends its owner:
  `runtime/api.send_to_strand_marking` with a mark under
  `client/notice/job/<id>` or `client/notice/execution/<id>`, so a steer
  into an open run or a fresh run on an idle strand, spent at most once.
  `beat` is the idle heartbeat's wake, an `api.prompt` that treats
  `StrandBusy` as success. `IdleClock` is the pure, volatile record of
  when each sampled owner went idle: a busy sample forgets the strand and
  a due beat restarts its stretch. Both owning services sample every
  `heartbeat_tick_ms` (a minute, slower than the hibernation interval).
  `docs/design-notes/async-completion-wake.md` has the design.
- `client/owner_services.{OwnerServices, JobsOwner, FactAccess, FactFault,
  OwnerCapCall, working_directory_prefix, served_prefixes, jobs_owner,
  local_facts, local_jobs_owner, local, no_capability, unavailable}` — the
  one record of plain functions a session's workspace half calls back into
  the owner through: `escalate` (a policy refusal's decision), `facts`,
  `output` (the rolling output tail), `capability` (an owner-bound
  code-mode call), `holds` (a strand's tool list), and `notify`,
  `strand_activity` and `wake` (the jobs actor's completion notice, activity
  read and idle heartbeat). Locally every function is the call it replaced;
  a workspace on another node is given the same record backed by messages.
  `FactAccess` is fenced to `client/working_directory/` and `job/` (a key
  elsewhere is `NotServed` before any store is consulted) and keeps
  `FactFault` distinct: `StoreAbsent`, `Conflict`, `LeaseStolen(held_by)`,
  `Failed(detail)`. `jobs.Wiring` takes a `JobsOwner` plus a `session_path`
  label supplier instead of a borrowed runtime. `notice.deliver` is the local
  `notify`; `notice.sample` takes the activity and wake functions, with
  `sample_runtime` as the shim `async_runs` keeps. `serve` builds the local
  record (`owner_services.local`) and derives `wiring.Config.escalations` and
  `observe_output` from it. Background code mode (`async_runs`,
  `async_codemode`) is not ported and keeps its `Runtime`.
- `client/cap_placement.{Placement, owner_caps, executor_caps, placement}` —
  where each code-mode capability name is answered when the workspace is not
  in the owner's VM, built from the routers' `serviced_caps` constants.
  `cap_placement_test` fails on a name in neither group or in both.
  `codemode.OwnerSide` is the four doors the owner's arms read (surface with
  its Agency, notes, schedules, MCP), `codemode.owner_side` reads them off a
  `Config` and `codemode.owner_serving` builds them from a seam setting, an
  Agency and a scheduling door with no `Config` at all, which is what an
  orchestrator of a remote workspace has. `owner_arms` reads the arms for one
  seam, `owner_calls` composes `strand.*` and `notes.*` from them for the local
  router, and `owner_answering` composes the same arms to answer an
  `OwnerCapCall` from plain data (`owner_capability(config)` is it over the
  Config's own side). It refuses a seam the surface does not serve, and its
  `beyond` argument wraps the whole chain outermost for arms a `Config` does not
  carry. `peer.*` is one: a local session composes it through
  `serve.with_code_mode_peers`, and the owner of a remote one through
  `owner_codemode.answering`.
- `client/owner_codemode.{over_owner, advertising_peers, answering}` — code
  mode when the workspace and its owner are on different nodes, both ends in
  one module. `over_owner(config, owner)` is the executor's `CodeModeAttach.arms`:
  it serves the shipped default of both seams over a stand-in Agency (every
  operation `AgencyUnavailable` except `holds`, which asks the owner), and puts
  one router outside the execution's that sends each name
  `cap_placement.placement` gives the owner as an `OwnerCapCall` through
  `OwnerServices.capability`, inside the `ServedHere` worker so the capability
  host keeps reading its channel. A dead port or dropped link answers
  `owner_unavailable` at once. `answering(side, peers:)` is the orchestrator's
  end, built in `serve` for a registered workspace from the session's own
  Agency, scheduling door and peer wiring. `advertising_peers` appends
  `peer.*` to every seam a tool offers, for `serve.code_mode_tool` and the
  executor. Background programs and MCP façades are offered (see "Background
  code mode and MCP on a registered session" below); workflow steps of a
  foreground program are not (the owner names the refusal). An operator's narrower
  `--codemode-seams` choice is enforced by the owner, since the executor is
  not told it.
  `owner_codemode_test` drives the capability router through a real owner port
  and link, and `daemon_shipped_remote_caps_test` runs the route across two
  daemons.
- `client/workspace_policy.{Basis, session_base, session_toolchain,
  base_policy_for, base_policy_fault, go_cache_fault, protecting_*,
  admitting_*, widening_*, allowing_*, under_tools_config, merging_mounts,
  tool_environment, session_environment, prepare_directories, degraded,
  lsp_server_roots, lsp_places, env_text, ...}` — the sandbox policy,
  environment and directory layout of a session's workspace, as functions of
  plain values (a workspace path, the `[tools]` table, the Go caches, the
  owner's protected files). They moved out of `serve` so that the workspace
  half of session assembly can use them without importing `serve`, and so
  that an executor holding only a workspace can run the same composition. `Basis` is the four values `session_base`,
  `session_toolchain` and `go_cache_fault` read; `serve.workspace_basis`
  derives it from `Settings`. `prepare_directories` takes the owner's
  directory as an `Option` first argument, so the workspace half does not
  need the session path. Nothing here reads a session, a store or a mailbox.
- `client/workspace_plane.{WorkspaceSpec, Prepared, Census, Warning, Attach,
  CodeModeAttach, Retain, Children, WorkspacePlane, PromptFacts, Started,
  Local, LspPlane, prepare, start, start_local, worktree_wiring,
  start_effect_plane, mixed_entropy}` — the workspace half of a session,
  built alone. `prepare(spec, reading:)` spawns nothing: it discovers the
  toolchain, composes the session base with `workspace_policy.session_base`,
  refuses a base the sandbox cannot enforce or a Go cache that cannot be
  honoured, makes the directories and reads the machine into a `Census`
  (workspace root as an opaque string, toolchain result, effective language
  servers with roots resolved, installed-extension discovery, platform,
  shell, base policy, tool environment and its unset names, Git program,
  the workspace's two hook settings files as bytes, warnings). `WorkspaceSpec` is plain data, including `owner_files` (the
  index, memory store and digest paths to mask, `None` when the owner is on
  another machine); `reading` is the one closure `prepare` takes, because
  `[tools] env` values resolve from the machine's own configuration.
  `start_local(prepared, attach)` takes the owner's contribution as an
  `Attach` (logger, namespace, `Retain` custody callback, `OwnerServices`,
  the session label for the jobs actor, and `CodeModeAttach` carrying the
  owner's arms of code mode and the `code_mode` tool constructor) and
  starts the helper pool, the executor service and the broker, publishing
  `Helpers` then `Broker` through `Retain` in that order. It recomputes
  `session_base` first, because masks over the owner's files depend on files
  that exist only after the owner's storage opened (the second computation
  is the one the returned census carries; a test pins it). It builds the
  scratch, jobs and language-server wiring, the code-mode configuration
  (workspace doors first, `CodeModeAttach.arms` last, so the peer router
  wraps the working-directory router as it did), the workspace's tools
  (`contributions.workspace_tools` then `directory_tools`, less the
  operator's deactivations) into the registry `run` dispatches over, and
  prepares the Git identity, whose warning joins the census. `decls` is that
  registry described, in registration order. `start` returns only the interface a remote workspace
  matches, `Started{plane, children, decls}`; `start_local` returns `Local`,
  which adds the pool, executor, language-server plane, code-mode
  configuration that `serve` still needs. `Children` is
  three adders (`scratch`, `jobs`, `lsp_manager`) because `serve` splices
  each into its services tree at its established position; each holds only
  its child specification. `WorkspacePlane` is `run(ToolRun, Authority)` (the
  workspace half of `wiring.run_tool`), `broker`, `census`, a lazy
  `prompt_facts` (the workspace's guidance files as text and the helper's
  health, so a pinned prompt pays for neither), `resolve_directory`
  (`directories.resolve_addition` against the workspace's filesystem),
  `live_jobs`, `fatal` (pool, executor, broker pids) and `close` (the
  language-server stop `close_instance` runs). A local plane never calls
  `OwnerServices.capability`: the owner's code-mode arms are composed into
  the router directly. `mixed_entropy` and `start_effect_plane` (the
  one-shot planes' entry, which `serve.start_effect_plane` delegates to)
  live here too.
- `client/tool_placement.{Placement, core_names, workspace_names,
  owner_names, placement}` — which half runs each built-in tool, as two
  lists of names built from the tool modules' own name constants.
  `placement(name)` is `Ok(WorkspaceSide)` for `bash`, `grep`, the file
  tools, `code_mode`, the `job_*` tools and `working_directory`;
  `Ok(OwnerSide)` for the agent family (with `todo`), `history_search`,
  `remember`, `schedule_*`, `context_remaining`, `load_skill`, `peer_*` and
  `advise`; and `Error(Nil)` for an extension's tool, which a local session
  runs on the owner and a remote one will refuse. `tool_placement_test` builds
  each half with every plane enabled and fails on a name built and not placed,
  placed on the wrong side, on both sides, or listed and not built.
- `wiring.{run_placed, WorkspaceRun}` — the routed tool surface. `serve`
  replaces the `run` slot of the built `Effects` with `run_placed(owner_run,
  run_holder, session, clock)`: a name `tool_placement` puts on the workspace
  reads its stored `Authority` and runs through the plane's `run`; everything
  else, owner-side built-ins and extension tools, takes the path every call
  took before. The plane's `run` closes over the workspace's registry, so it
  is held in a second `tool_holder` (started with the first by `start_holders`
  and retired with it under `custody.ToolConfig`) for the reason the
  configuration is: the effect surface is copied into every process that holds
  the runtime. The owner's registry holds the workspace's tools only as
  `contributions.described_tools(decls)`, `tool.Described` with a `run` that
  refuses, built by `tool.from_described`; a routing mistake meets that
  refusal, never a tool. `ToolSurface.recover` stays `None`.
- `client/hookserve.{Contents, read_contents, gather, load_from}` — imported
  hooks over sources read on another machine. `read_contents` is one file as
  `Missing`, `Bytes` or `Unreadable(reason)`; `gather` pairs each located
  source with its contents, reading the operator's file on the owner and
  taking the workspace's from the census by path; `load_from` is `load` over
  those pairs, so parsing, the trust check against the owner's record and the
  merge are the same function whichever machine the bytes came from. `load`
  is `load_from` over files read here.
- `client/directories.{resolve_addition, admin_over}` — the half of an
  operator's `add-dir` that needs the workspace's files, and the door built
  over a resolver for it. `admin_with_facts` is `admin_over` with
  `resolve_addition` over the session's workspace; `serve` passes the plane's
  `resolve_directory`.
- `client/system_prompt.{discover_user, discover_workspace, render_guidance}` —
  `discover` split at the machine boundary. The operator's global `AGENTS.md`
  is read on the owner; the workspace's `AGENTS.md` and `CLAUDE.md` arrive
  from the plane's `prompt_facts` as text, and `serve.render_prompt` renders
  the same document the single lookup did (a test pins the equality).
- `client/contributions.{workspace_tools, directory_tools, OwnerTools,
  owner_tools, compose}` — `built_in` is now the workspace's tools (the five
  core tools, `code_mode`, the `job_*` tools) composed with the owner's
  (`agent_*`, `history_search`, `remember`, `schedule_*`,
  `context_remaining`). `compose` cuts the workspace's list after the run of
  core tools and after `code_mode` and wedges the owner's two runs in, so the
  registration order (the pinned prompt's tool index) is what it always was
  whichever tools are absent. `tool.Described` is a tool's name, description,
  snippet, schema, replay and execution mode: what a registry needs to offer a
  tool whose behavior is on another machine.
- `client/jobs.{JobsPolicy, Request, Audience, Released, Started, Cursors,
  Polled, Listed, Refusal, Spill, Wiring, Message, StdinEnd, Control, Ask,
  max_jobs_per_strand, default_wall_ms, tail_bytes, settle_grace_ms,
  stop_grace_ms, runner_ask_ms, default_policy, parse_policy, blob_spill,
  staging_path, start, supervised, start_job, release_job,
  sample_heartbeat, poll_job, list_jobs, kill_job, write_stdin,
  await_job, await_completion}` — the session's background
  jobs: one `weft/actor` in the **restartable** services tier beside
  `client/extension/hosts`, and one weft runner per job. The actor owns
  the durable `job/<id>` record, the per-strand ceiling of four, and the
  closures that cancel and write stdin; the runner owns the jailed
  execution, the two `tools/tail` windows and the staging files.
  **The runner and not the actor calls the broker**, and two clauses of
  the broker's contract force it: `clear_call` waits out a full helper
  pool in the *caller's* process, so an actor that cleared would stop
  answering polls the moment the pool filled; and the relay monitors
  whoever owns the events subject and cancels the execution when that
  process dies, so the owner has to be a process whose life is exactly
  the job's. A live job's output is therefore read by a **monitored
  call** from the actor into its runner, and a finished one's from the
  tails the runner handed back with its final report. The terminal fact
  is written when the runner's **weft outcome** arrives, never when its
  settlement message does: an outcome is reported only after the worker
  exits, so the scope's exit is the drain proof. The task is a plain one
  rather than a managed one, deliberately — a managed task witnesses
  owners a worker *discovers*, and this worker discovers none. Admission
  claims the cell with `api.put_reserved_fact_expecting(expected: None)`
  *before* the clearance (the effect sandwich applied to a spawn) and
  **deletes it** if the clearance refuses, because a refusal is not a
  crash and a job that never existed must not hold a ceiling slot.
  Attribution never rests on message order: the runner bounds its own
  fold at the deadline and both tells the actor *and* carries the cause
  in its final report, since those two reach the mailbox from different
  senders. On start, an injected `actor.continuing(Reap)` sweeps `job/*`
  and commits `Lost(VmRestart)` for everything still live — for **both**
  restart cases, because telling this session's first start from a
  supervisor restart needs state that outlives the actor and dies with
  the VM, which is machinery bought for a word nobody branches on.
  Production waits use `WatchOne` to obtain an ownership-checked completion
  witness alongside the current `Polled`. The first waiter starts one linked
  weft actor; later waiters share it without registering their subjects.
  `commit` stops it after applying the terminal event and attempting the fact
  write. Each caller monitors it and rereads `poll_job` on DOWN or timeout;
  DOWN itself never means success. A monitor installed after completion also
  wakes, and the witness follows its jobs-owner parent on restart or kill.
  Refused clearance stops it before deleting the held record. Terminal jobs
  and swept records retain no witness. Persistence errors retain the existing
  in-memory commit behavior; a wake proves write-attempt ordering, not successful
  storage under an owner failure.
  **Each job's `Audience` decides who hears about its end.** `NotifyOwner`
  (a background `bash`) sends the owner a completion notice after the
  terminal commit; `CallerWaiting` (an auto `bash`) is silent until
  `release_job`, or until the caller the actor monitors from admission
  dies; `ProgramWatches` (`cap/job`) never notifies. The notice goes out
  from the actor's own settlement handlers and from the restart sweep for
  the jobs it just declared lost, never from the session-stop drain. An
  owner's kill, an operation abort and a session stop are silent. A
  `Beat` tick every minute samples owners of live jobs for the idle
  heartbeat under `JobsPolicy.heartbeat_ms` (`[jobs].heartbeat_s`). Only
  a job whose `Request.idle_wake` is `tools/job.WakeWhenIdle` counts, and
  the listing names only those jobs: the heartbeat is opt-in per job, via
  `bash`'s `heartbeat` argument, so a passive watcher never wakes its
  owner. The choice is held on the actor's in-memory `Held` rather than
  the durable record, since no job is live across a restart.
- `client/jobseam.{Wiring, Door, max_wait_ms, ask_timeout_ms,
  start_margin_ms, first_slice_ms, max_slice_ms, door, production, none, real_rest}`
  — the host side of the model-facing jobs door, mirroring
  `client/scheduleseam`: six closures keyed on the caller's strand,
  which `client/jobtools` turns into `tools/job` and `cap/job` surfaces.
  Ids are text here
  and typed behind it, and this is the one place a string becomes a
  `jobstate.JobId` — with `Invalid` rather than `NotFound` for a
  malformed one, because "that is not a name a job could have" is a typo
  to fix and not a job to stop looking for. `workspace_plane` selects
  `production`, whose positive waits call `jobs.await_completion` on the real
  monotonic time base. `door` retains the injected clock and rest over
  `weft/poll` for simulations. Both wait in the caller's process, so a model
  blocking thirty seconds on one job cannot stop other actor requests.
- `client/jobtools.{seam, capability_door, refusal, started, polled,
  listed}` — the translation between `client/jobseam.Door`'s vocabulary
  and the model's. `tools` may reach only `core` and `broker`, so neither
  package may name the other's types and something has to sit between
  them; `client/scheduleseam` needs no such module only because that door
  was written in `tools/schedule`'s words to begin with. Two entry points
  and one translation: `seam` fills the `job_*` tools and is keyed on a
  `tools/tool.Ctx`, `capability_door` fills `codemode/workspace.JobDoor`
  and is keyed on the strand and the operation an execution belongs to,
  because a capability call has no `Ctx`. Both bind the owning strand
  from the caller and both carry the caller's **real** operation, since
  that is what `broker.abort` addresses and therefore what decides
  whether an operator's abort reaches a running job.
- `client/codemode.{over_mcp, seam_allowlist, seam_caps_on}` — what a
  configured MCP server does to the seam a model is offered. One
  `Config.mcp` field, for the reason `surface` is one field: a server
  widens each installed program mode's *allowlist*, its rendered *description*,
  the *generated table* the hermetic build takes, and the capability
  *router*, and a host that could set those apart would eventually set
  them apart. The default workspace and orchestration modes both route the
  configured MCP façade; an explicit workspace-only host stays effect-only.
- `client/codemode.{Surface, Seams, serving, orchestrating,
  surface_seams, surface_seam, seam_policy, seam_caps, tool_seam,
  vetting_seam}` — which code-mode seams this host serves.
  `Surface` is `Workspace`, `Orchestration(agency, spawn_ceiling)` or
  `Both(...)`: one field, because the vetting allowlist, the capability
  router and the admission ceilings have to agree and a host that could
  set them apart would eventually set them apart. `Seams`
  (`WorkspaceOnly` / `OrchestrationOnly` / `BothSeams`) is the same
  choice as a *setting* — what a flag can carry before there is an
  `Agency` to serve the orchestration seam with — and `serving` turns
  one plus an Agency into a `Surface`; `orchestrating` is the
  orchestration-only case of it. `seam(config)` publishes exactly the
  seams the surface serves as `tools/codemode.Seams`, each with the
  allowlist and the serviced capabilities read off the running policy,
  so what the model may ask for and what will judge it cannot drift.
  `tool_seam` / `vetting_seam` are the two directions of the mirror
  between `vet/policy.Seam` and the tool's own `Seam`.
- `client/mcp.{Layer, Server, Refusal, Options, none, serving, start,
  stop, routing, allowed_imports, surfaces, generated, serviced_caps,
  listings, sha256_hex}` — the MCP layer (#106): the configured servers
  this host actually reached. `start` fans every `catalog.McpServer`'s
  bring-up out over a `weft` run — one task per server, bounded by the
  catalogue's own length — rather than folding them one at a time, so an
  N-server boot pays one shared clock for every handshake timeout rather
  than N consecutive ones; `weft.start` hands outcomes back sorted by
  input position, so zipping them against the unchanged `servers` list is
  what restores catalogue order regardless of which server answers first.
  Each task still spawns its server over `gleam_mcp/transport.PortTransport`,
  hand-shakes, lists its tools and generates its `cap/mcp/<server>`
  façade, keeping the client **running** for the session because dispatch
  is the same client; each failing step refuses *that* server with a
  worded `Refusal` and the boot goes on without it, and a `Crashed` weft
  worker becomes a worded `Refusal` too rather than taking the boot down.
  `routing` is the capability arm: a cap under `mcp.` is answered as
  `satellite.ServedHere` — no `CallSpec`, no jail, no policy — and
  everything else is handed to the router beneath untouched. The client
  actors are started from a throwaway unlinked process on purpose, two
  `process.spawn_unlinked` calls below `start_one` itself, so a
  third-party server's client cannot fell the server; that isolation does
  not depend on which process runs `start_one` — a weft worker moves who
  calls it, not how the actor is severed from its caller, so bring-up
  needed no re-link back to the boot's own process and no accessor onto
  `gleam_mcp/client.Client` to do one. `Layer` is then the only handle on the
  clients, and `serve.shutdown` is what stops them.
- `client/system_prompt.{Host, Rendered, Assembled, Origin, assemble,
  render_pack, pack_source, guidance, pinned_in, pinned, pin}` — the I/O
  half of the pure `prompt` package. `Host` is every `pack.Environment`
  field gathered from a real source; `pack_source` picks the shipped pack
  or the one `LOOM_PROMPT_PACK` names; `render_pack` renders it or
  refuses; `assemble` chooses between the `LOOM_SYSTEM_PROMPT` override,
  the pinned cell and a fresh render; `pinned_in`/`pinned`/`pin` are the
  two ends of the reserved `prompt/` cell. Nothing here is called per
  turn — the whole module runs once, at boot.
- `client/system_prompt.{GuidanceFile, GuidanceOrigin, discover, guidance,
  render_file, origin_name, agents_file, claude_file,
  user_default_directories, max_guidance_file_bytes}` — the session's
  instruction files and the fence they reach the model behind. `discover`
  fills three slots in render order: the operator's global `AGENTS.md`
  (under `~/.agents` then `~/.loom`, first found wins), the workspace's
  `AGENTS.md`, and the workspace's `CLAUDE.md`. `guidance` joins them into the one string
  `Host.guidance` carries, each file inside an `<instructions>` fence
  naming its path and its `origin_name` — `workspace` or `user-default` —
  which is the vocabulary the pack's framing prose is written against.
  The home directory is a parameter rather than a `HOME` read, so the
  whole lookup is testable without a process environment; `serve` reads
  `HOME` and passes it in.
- `client/wiring.{request_target, resolved_target, strand_thinking_level,
  thinking_level}` — the model-routing half of the seam: which role (if
  any) a captured identity is on and therefore whether the dispatch walks
  a chain, the `ForResolved`-only target every deferred poll and every
  off-route generation takes, and the two directions of the map between
  the machine's seven-point thinking scale and the provider's four-point
  one — `thinking_level` collapsing for a dispatch, `strand_thinking_level`
  lifting an entry's declared level to seed a strand.
- `client/wiring.{Config, build_effects}` — the production effect seam:
  a `runtime/effects.Effects` over the real provider gateway, broker,
  and tool registry (promoted from `conformance/wiring`; spec-gaps M2
  item 7). The conformance wiring/e2e suites still prove it.
  `Config.escalations` is the escalation seam a policy refusal goes
  through — from `escalating_runner` for a refusal the broker handed
  back, and from the `Ctx.raise_refusal` seam for one a tool met
  elsewhere; both build the same `escalate.Refused` from the same
  `ToolRun`. There is deliberately **no** session-wide grant list,
  because a grant that cannot be attributed to the call in hand must
  widen nothing.
- `client/wiring.{run_tool, terminates}` — the tool-dispatch boundary and
- `client/vision` — the vision routing rule (issue #358): a request
  whose current turn carries an attached or tool-result image, on a strand whose catalogue
  entry is text-only, dispatches through the routed
  `vision` chain (`ForRole(Vision)`, admitted and accounted against
  the vision head's own facts) or is refused in band at admission with
  a worded reason — `image_unsupported` when no chain resolves,
  `vision_misconfigured` when the routed head is itself declared
  `TextOnly`. Every other request to a text-only identity carries its
  `UserImage` and `ToolResultImage` blocks replaced with text placeholders, in the transient
  projection only; the durable transcript keeps the images. The
  classifier walks the current turn from the latest attributed human
  prompt or settled assistant boundary. Digests have no origin and tool
  steps do not close a turn. A disk image returned by `fs_read` switches the
  next request to vision and subsequent tool steps retain that route.
  Human attribution also survives projection of failed assistant responses,
  allowing the next text prompt to end a rejected image turn. The immutable
  operation admission batch is checked as well, including captured inputs
  before its explicit prompts. An image-bearing admitted run stays on vision
  through tool and run-end continuations, so a held image followed by a text
  instruction cannot be silently placeholdered. A new text-only run does not
  inherit the previous run's image requirement. Explicit `vision` declarations override the built-in capability.
  Before preparing dispatch, wiring counts the original user and tool-result
  images after the operation's source leaf and gives the gateway a request-local
  protected count. The scan crosses compactions through their preserved parent
  links, ignores copied retained tails and stops at the immutable run boundary.
  The gateway budgets historical images independently for every fallback model
  and refuses an oversized active turn locally. A successor run excludes old
  images from protection; compaction cannot reclassify retained history as new.
  GLM-5.3 (with or without the `zai-org/` prefix) defaults to text-only;
  GLM-5.3-Flash remains distinct. Unknown identifiers retain the legacy
  image-capable default. A following attributed text prompt can recover from a failed image request
  with historical image placeholders, even after projection removes the error.
  The catalogue parse refuses a `vision` chain that names a
  `vision = false` entry, so a retryable walk cannot deliver an image
  to a model that cannot read it. `Config.facts` carries the entry's
  `catalog.ImageReading` beside its resolved facts and api, so the
  capability and the accounting read one seam and cannot drift.
  the one conversion across it. `run_tool` always settles as
  `effects.ToolCompleted`, and its `terminate` is now the outcome's own
  `tool.Terminate` rather than the hardcoded `False` it was: `terminates`
  is where `TerminateRun` becomes the `Bool` the frozen effect type
  carries, and it is the only place the polarity is written down.
- `client/wiring.{compaction_hooks, strand_window, resolution}` — live
  compaction's host half, separable so a host with its own provider
  surface can run the real hooks. `compaction_hooks` builds the whole
  `effects.Hooks` record through `runtime/hooks`: real admission from
  the gateway's resolved model facts, threshold and overflow over the
  strand's durable projection, the notes checkpoint as every structural
  decision (`VerdictSupplied`, no usage; a branch summary and an
  unbuildable checkpoint decline), and the near-limit reminder on the
  `context` slot. `strand_window` is the window a strand is measured
  against — its own catalogue entry, else the fallback — exposed so
  `serve` can build the `context_remaining` seam from the same rule
  before the config exists. `resolution` asks whether the configured
  role still routes. There is no summary request, no sink and no
  progress hook: no summarizer serves this host, and a `SummaryRequest`
  reaching `dispatch` is refused terminally.
- `client/checkpoint.{Recall, Closed, Checkpoint, max_notes_bytes,
  header_prefix, instructions_fence, reminder_prefix, for_operation,
  render, reminder_point, reminder, reminder_text, remaining_seam}` —
  the context checkpoint: what replaces the older half of a strand's
  context when its window fills. `for_operation` builds it from durable
  state alone — the operation's frozen preparation, the strand its
  `op.meta` names, that strand's `agent/{strand}/` cells newest first,
  the compactions already on its branch (the window ordinal) — and
  `render` is the text: a `[loom]` header naming the window, the counts
  and where the cut messages went (`history_search` when `Recall` says
  the strand can call it), the notes quoted as data under
  `max_notes_bytes`, and an operator's `compact` instructions quoted in
  their own fence. A prior checkpoint is named by session and entry ID
  for exact retrieval of inherited context. `reminder_point` is one reserve below the threshold's
  cut, `reminder` the user message the `context` slot appends past it;
  that slot counts the messages the request already carries rather than
  reading the branch again, which cost a scan and decode per request
  (issue #359).
  `remaining_seam` fills `tools/context`'s seam from the same projection
  and token fold the threshold reads. `docs/architecture/compaction.md`
  carries the argument for a checkpoint over a summary.
- `client/wiring.compaction_hooks` enables exact retained-tail references only
  when its registry contains `history_search` and the durable strand
  configuration activates that tool. Threshold and overflow preparations share
  this gate. Manual compaction applies the same gate at the gateway boundary,
  so custom and restricted tool surfaces retain payloads verbatim.
- `client/serve.{default_reserve_tokens, default_keep_recent_tokens}` —
  pi's compaction defaults, and the only place they are stated.
  `LOOM_COMPACTION`, `LOOM_COMPACTION_RESERVE` and
  `LOOM_COMPACTION_KEEP_RECENT` override them; settings that cannot
  describe a working compaction disable it rather than firing a
  threshold that prepares nothing.
- `client/contributions.{Origin, Contribution, Collision}` — who
  contributed a tool (`BuiltIn` | `Extension(name)`), one origin's
  ordered tool list, and the refusal when two contributions claim one
  name. `code_mode` is `BuiltIn` and gated on its plane, exactly as
  `history_search`, `remember` and the `schedule_*` tools are.
- `client/contributions.built_in(Option(Agency), Option(CodeMode),
  Option(History), Option(Memory), Option(Schedules), Option(Context),
  Option(Jobs), Option(lsp/query.Door), List(#(String, String)))`
  — the host's own single contribution: five core tools, plus the six
  `agent_*` tools only when a messaging plane exists, plus `code_mode`
  only when this host wired a code-mode pipeline, plus `history_search`
  only when its search index opened, plus `remember` only when the
  memory session beside the session file opened, plus the three
  `schedule_*` tools only when the schedule store did, plus
  `context_remaining` over `client/checkpoint.remaining_seam` — the one
  seam every served session has, so its `Option` is for a registry built
  with no session behind it. A plane that is absent contributes nothing
  at all. When code mode or jobs is available, the built-in `fs_read` also
  receives `codemode.cap_scheme` or `job.scheme`, respectively. The schemes
  reuse those planes and add no separate registry entry. A language-server
  door (ADR-015 §6 and its code-mode addendum) builds `fs_write` and
  `fs_edit` with `tools/lsp.diagnostics_observer`, so a landed write's
  result gains its settled diagnostics; with `None` the two write tools are
  the plain ones and the definitions are byte-identical to a host that
  never heard of language servers. `serve` passes the session manager's
  door when the catalogue configures an `[lsp.<name>]` server that
  survived its load, and `None` otherwise (see "Language servers"). The
  last argument is the served profiles' hints as `#(server name, hint)`,
  attached to offers that admit and serve `cap/lsp`. The `code_mode`
  description and `cap://lsp` read carry them with the native API. The default
  registry does not register top-level `lsp_*` tools. Without a served LSP
  capability it advertises no naming hints.
- `client/contributions.registry(List(Contribution)) ->
  Result(Registry, Collision)` — the seam an installed extension enters
  the registry through. Last-registration-wins survives *inside* one
  contribution; a name two contributions both claim is refused, so an
  extension can shadow neither a built-in nor a peer. `serve.boot`
  turns a `Collision` into a boot refusal through
  `contributions.collision_message`, which names both origins.
  `contributions.deactivate(contributions, names)` runs first and drops
  those names from the **built-in** contribution only, which is the whole
  of how an extension's tool comes to stand in for a built-in one: an
  active built-in still collides, a deactivated one leaves its name free.
  `serve.Settings.deactivated_tools` fills it from `LOOM_DISABLE_TOOLS`. What a
  registry produces reaches a session once: the prompt index and
  `active_tool_names` are both fixed at session creation, so an
  extension installed later is seen by the next session, not this one.
- `client/workspace_policy.session_base(Settings, String, String, String,
  Result(Toolchain, String))` — the whole composed session base in one
  named function: the index and memory protections, `allowing_tool_tmpdir`,
  `allowing_imported_hook_env`, `under_tools_config`,
  `widening_linked_worktree`, `admitting_codemode`, `merging_mounts`, in
  that order. `assemble_in` calls it and `base_policy_fault` validates the
  result. It is a function rather than an inline pipeline so a test can
  read the composed allowlist back — `policy.meet` intersects `env_allow`
  against this, so a step left out is not a missing convenience but every
  call that wanted the name refused.
- `client/workspace_policy.protecting_index(SandboxPolicy, String)` — the base
  policy with the search index added to `protected`. A security property,
  not hygiene: snippets from that index are read back into *future*
  sessions' contexts, so an index a model can write is a channel from one
  execution's output into a later execution's input. `protected` bars
  writes; since ADR-019 it bars native reads as well, matching the jail's mask.
  `assemble` composes it before `base_policy_fault` validates, so the
  addition is checked by the same gate every other path is.
- `client/workspace_policy.protecting_memory(SandboxPolicy, String, String)` — the
  same bargain one step along, over `loom-memory.db` (with its WAL
  family) and `loom-memory.digest`. More direct than the index's: a
  search snippet reaches a later session only if a model searches for it,
  while the digest is injected into **every** run of every session on the
  repository, unasked. Both paths are conditional — masked once they
  exist, or before that where a writable root reaches them — because the
  jail refuses to mask a *missing* protected path under a read-only
  parent and neither file exists until a distillation run has happened.
  The two functions share one `protecting(always:, where_maskable:)`
  mechanism rather than each carrying a copy.
- `client/workspace_policy.state_root_mask_candidates(String)` and
  `client/workspace_policy.protecting_state_root(SandboxPolicy, String)` — what the
  daemon masks under its state root, and the composition `resolve_managed`
  applies in place of the root. **The state root is not itself a mask**,
  and that is the invariant: masking `~/.loom` wholesale reads as prudence
  and broke a workspace an operator had every right to open on it — a
  session created there to edit `loom.toml` got a Seatbelt profile denying
  reads over the jail's own working directory, so every jailed `bash`
  answered `getcwd: cannot access parent directories`, `ls` printed
  nothing, and the code-mode satellite could not open `.`. Each entry is
  decided on one question — could a jailed process reading or writing it
  obtain a credential, another session's data, or the daemon's control?
  Masked: `owner.token`, `tokens/`, `sessions/`, `catalogue.db` with its
  WAL family, `workspaces/`, `domains/`, `locks/`, `daemon.lock`,
  `launch.lock`, `endpoints/` and `daemon.endpoint` (those last two carry
  no secret and are masked as *control*: a launcher adopts a running
  daemon by the PID and birth marker it reads there), and `run/`, the
  code-mode socket root, which only a satellite's own base reopens for
  its one directory (`client/codemode.reaching_socket`). Not masked: the
  `loom*.toml` catalogues, which name environment variables rather than
  holding secrets, `extensions/`, `logs/` and `daemon.log`. The blob store
  needs no entry: it is `blobs/` inside the session's domain directory
  (`workspaces/<digest>/` or `domains/sessions/<id>/`), so the `workspaces/`
  and `domains/` masks already cover it, and the base policy protects
  nothing inside the workspace. `workspaces/` and `domains/` are created by `daemon/root.directories`
  and sit in `established_masks`, so a session admitted before the first
  domain of either kind still masks both. Adding an entry that the daemon
  creates lazily means adding it to `lazy_masks`, not `established_masks`: the
  jail refuses to mask a *missing* protected path under a read-only
  parent, and that refusal is a refusal of every jailed call. `lazy_masks`
  is filtered on **existence or a writable root**, never on writability
  alone — an ordinary workspace grants no writable root over `~/.loom`,
  so a writability-only filter left `tokens/`, `workspaces/` and the
  catalogue WAL readable from every jail. The public
  `state_root_mask_candidates` is the unfiltered list; `protecting_state_root`
  is what a session actually gets.
- `client/workspace_policy.build_plane_policy(writable, state_root)` — the base an
  extension install's jailed build runs under, and the second place the
  state root has to be masked. The extensions root is
  `<state_root>/extensions` by default, so the build runs one directory
  below `owner.token`. It grants no readable root beyond the staging
  directory: `start_build_plane` discovers the toolchain *before* it
  builds this base, then admits the discovered toolchain mounts onto it, so a compile reaches what it needs by name
  rather than by inheriting the host. Every state-root entry goes in **conditionally**
  here, `established_masks` included, because an install may be the first
  thing that ever runs on a host and a mask over a path a daemon has not
  yet written, under a parent the narrowed build may not write, is the
  refusal that took every jailed compile with it in #304. No blob mask is
  constructed either: an install has no content-addressed store, and a
  session's store is in the state root, not in a root the plane can write.
  `serve.start_build_plane` therefore takes a `state_root` argument, and
  `client/extension/cli` derives it as the parent of the extensions root,
  which is the inverse of `record.root_for`.
- `client/workspace_policy.admitting_codemode(base, discovered)` — the base half of
  the code-mode mount plan. The toolchain is located *before* the base
  policy is built (`workspace_plane.prepare` makes the discovery call and
  `serve.code_mode_mcp` only reports it) because the base has to carry what a
  satellite requires: mounts compose as the meet by path, so a base built
  without them would refuse every code-mode launch. Both halves read
  `client/codemode.toolchain_mounts`, so they cannot drift. A host with no
  toolchain is left unchanged, because it registers no `code_mode` tool
  and will launch no satellite. A toolchain prefix overlapping a
  `protected` entry — a seed unpacked inside the daemon's state root, say
  — is refused at boot by `base_policy_fault` naming both paths, which is
  the same treatment `broker/policy.validate` gives every mount-over-mask
  pair.
- `client/workspace_policy.admissible_toolchain(discovered, base)` — the step in
  front of `admitting_codemode`, in both the session assembly and
  `start_build_plane`. It turns a discovered toolchain into an `Error`
  when one of its mounts would shadow a writable root of `base`
  (`codemode.clear_of`), so the base never carries the mount — which
  `validate` and the helper now refuse (`protocol-change/057`), and which
  would otherwise refuse the whole boot — and no `code_mode` tool is built
  while `serve.code_mode_mcp` logs the sentence. The session asks a base assembled with
  the toolchain already in it for its writable roots, which is the same
  answer, because `admitting_codemode` changes mounts and no root. An
  operator's `[workspace] mounts` line naming a read-only ancestor of the
  workspace is *not* filtered: `base_policy_fault` refuses the boot naming
  both paths, the treatment every written mount gets.
- `client/workspace_policy.{base_policy_for, admitting_config_mounts}` select the
  session's filesystem view. `catalog.ReadScope` defaults to `HostReads`,
  with readable root `/`; `WorkspaceReads` selects the minimal helper view.
  The trusted `[workspace] read_scope` setting and daemon `--read-scope`
  flag select between them. There is no language-specific admission list
  and session assembly does not infer read grants from package manifests.
  Writes remain scoped to the workspace, linked Git metadata, and explicit
  writable mounts. Native reads (`fs_read`, code mode's `fs.*` and
  `search.*`) are judged by the same policy value: `codemode.workspace_seam_*`
  and `search_seam_*` take the session base policy and widen it with the
  request's additions (ADR-019). HOME and TMPDIR remain under the workspace, so build
  caches do not require host-home writes. A configured mount overlapping
  a protected path refuses assembly. Protocol-change/020's addendum records
  why the minimal view is now an explicit restriction.
- `client/workspace_policy.base_policy_fault` refuses a **workspace inside a mask**
  as well as a policy `broker/policy.validate` rejects. `protected` is the
  policy's only subtractive verb and no grant carves a hole in one, so a
  writable root at or under a masked entry yields a session that comes up
  and then fails on its own working directory. It is refused at boot and
  at session creation instead, naming the entry, so an operator who points
  a workspace at `~/.loom/sessions` is told to choose another directory
  rather than discovering it from the first tool call.
- `client/serve.Settings.codemode_seams` — which code-mode seams this
  server offers (`--codemode-seams workspace|orchestration|both`,
  default `WorkspaceOnly`). A setting for the same reason
  `base_policy` is one: the choice belongs to whoever stands the server
  up, and the Agency the orchestration seam routes onto does not exist
  until `boot` has built it. An unrecognised flag value is a usage
  error, not a fallback — a typo that quietly served the workspace seam
  would look exactly like a server ignoring the flag.
- `client/serve.Instance` owns one session's runtime, broker, helper pool,
  MCP layer, gateway and composition-service supervisor, `plane:
  WorkspacePlane` (what the owner holds of the workspace half), and `lsp:
  Option(workspace_plane.LspPlane)` — the language-server manager's handle,
  the helper-lease counter and the servers' attribution operation, `None`
  with no configured server. `pool`, `executor`, `broker` and `lsp` stay
  populated from the local plane for the teardown paths and the tests that
  read them.
  Its `namespace` owns the 11 reclaimable service addresses and is retired
  after the services stop. `prompt` retains the exact assembled prompt;
  `helper_path` identifies the executable used by this session. Optional
  `rulescan`, `schedulescan` and `memory_pass` addresses exist only when
  their services are configured. `stops` belongs to the caller that opened
  the instance. `open_instance` and `close_instance` expose this assembly
  without a listener, token file, token directory or signal handler.
  Each open generates a fresh random SQLite writer-owner identity. This
  identity differs from the stable saved session ID: clean close removes
  the lease row and permits fence one to recur, but an expired connection
  must never match the replacement's owner-and-fence pair.
- `client/serve.{Resident, resident, resident_children, drain_resident}` — the
  projection of a published `Instance` that the daemon instantiates
  `daemon/manager` with, so it is both what the manager keeps in `Book.slots`
  for every admitted session and what `ResolveIncarnation` replies with on
  every websocket upgrade. Three fields: the hub `gateway` (all an attaching
  socket reads), the fatal `children` (read once by the assembly host that
  monitors them), and an `api.Drain` — the tree and session store a graceful
  drain reaches strands through. None of the three grows with the host's
  configuration, and none of them reaches `api.Runtime.effects`; the whole
  `Instance` stays in the assembly host's build, which does not retain it
  after publication. `drain_instance` is now `drain_resident` applied to the
  projection, so the embedded and daemon drains remain one path.
- `client/serve.Booted` adds `served`, `token_path` and `bind_host` to an
  `Instance`. `boot` retains the current single-session entry point while
  sharing the same assembly. Listener setup failure closes the completed
  instance. This is not yet daemon admission: partial-boot and owner-death
  custody remain unfinished, and `close_instance` still discards the runtime
  drain result instead of returning a reservation-release verdict.
- `client/host.{Stop, Host, adopt, retire, relay_sigterm}` — the root of
  the server's process tree. `adopt` runs a boot on a dedicated
  exit-trapping process so every link an `actor.start` forms lands there
  rather than on the caller, and hands the boot the `Host` it runs on;
  the first fatal death runs the teardown *first* — releasing the session
  lease — and reports `Faulted` afterwards, leaving the exit status to
  the entry point. `retire(host)` asks the host for the same teardown and
  returns when the host has exited, so the teardown runs once, on the
  host, however it was started; `serve.shutdown` is exactly that call.
  `relay_sigterm` puts the signal on the same subject, so one receive
  covers both ways the server stops.
- `client/serve.boot_with(settings, logger:)` (`@internal`, like `boot`;
  both start the legacy listener above) — `boot` with an injected
  `telemetry/log.Logger`, which is what `main` calls once it has
  installed the JSON handler. The logger is a capability, not a setting
  (§0.2): it is passed rather than parsed, and `boot` itself delegates
  here with `log.discard()` so an embedding test boots a whole server
  without a handler existing at all.
- `client/serve.{Settings, boot, shutdown, main}` — the server entry
  point (`gleam run -m client/serve`, `bin/loomd` via the erlang
  shipment): flags/env in, session + helper pool + broker + system
  prompt + runtime + service supervisor + websocket server up, one
  startup line out, and either `SIGTERM` or a fatal fault runs the same
  `shutdown` so the lease is released rather than left to its TTL.
  `client.main` delegates here for the shipment's entrypoint.
- `client/serve.Settings.demand` — the broker's enforcement contract.
  `main` selects `PlatformEnforcement` by default, which is strict on Linux
  and admits only ADR-006's three reported Darwin gaps. The
  `--full-enforcement` flag asks for Linux-equivalent containment on every
  platform; `--best-effort` explicitly accepts any honest degradation. The
  two flags are mutually exclusive.
- `client/serve.{helper_ladder, seed_ladder}` — the two lookup orders,
  taken as rungs rather than performed inline, because *order* is the
  whole of what they decide and a lookup against the host running a test
  cannot show an order. `helper_ladder` is `--helper`, the tree this
  server shipped in, `PATH`, `./bin`; `seed_ladder` is `--codemode-seed`,
  the workspace's own seed, the bundled one, and a named path for the
  refusal to point at when nothing answers. The workspace rung is
  `serve.usable_seed`, which admits a snapshot only after `seed.verify`
  accepts it; a local daemon and an executor (`executor_plane.seed_of`) both
  use it.
- `client/workspace_policy.{shell_path, base_policy, degraded, helper_probe_ms}` —
  the host facts the system prompt and the jail must agree on: the shell
  jailed commands run under, the *default* session base policy, and the
  one question the prompt has no other source for — whether a helper's
  hello advertises `degraded`, asked once at open by borrowing from the
  pool the session will use anyway.
- `client/serve.Settings.helper_pool_size` — how many `loom-exec`
  helpers may run at once, and therefore the real ceiling on how wide a
  parallel tool batch runs. `resolve` fills it from `LOOM_HELPER_POOL`
  or `exec.default_pool_size()` (schedulers online), never a literal, and
  clamps *both* to `[exec.min_pool_size, exec.max_pool_size]` = `[4, 16]`.
  Both ends are load-bearing: below two, code mode cannot run at all (a
  satellite holds one helper for the node while the program's capability
  calls ask for another), and above sixteen an operator's typo would ask
  the host for that many live bwrap jails. A batch wider than it waits for a slot
  inside `broker.clear_call` rather than coming back as a resource
  error. Distinct from the broker's pooled `max_outstanding`, which
  refuses amplification rather than describing what the host affords.
- `client/workspace_plane.start_effect_plane_in` — a session has one execution model,
  the executor service (issue #696). There is no lane setting: S3 deleted
  `ExecutorLane`, `Settings.executor_lane` and `LOOM_EXECUTOR_LANE`, so a
  session cannot be opted back into the broker's per-call relay. It builds
  the pool, then starts
  `executor.start` over closures on that pool (`custody`, `checkout`,
  `checkin`, `close_helpers`) and the session's `Logger`, which is where the
  service's `executor.settled` and `executor.closed` lines go, makes
  `executor.close(service, waiting: 5000)` the `Helpers` custody step (it
  drains executions and returns the pool's own verdict), unlinks pool and
  service together, and gives the broker `executor.dispatcher(service)` via
  `broker.start_dispatching`. `Instance.executor` carries the service and
  `instance_children` lists it as "the executor service", a fatal root: a
  replacement could not be reached by the closures already holding the old
  one. The boot-time `degraded` probe still borrows from the pool directly,
  before the broker serves anything. `start_effect_plane`, which the build
  plane, the check plane and the extension installer use, has no custody
  instance to publish into but runs the same model: it starts the pool, the
  executor service over it (`workspace_plane.start_service_lane`, with a
  discarded logger and no custody)
  and a `broker.start_dispatching` broker, and returns all three. The
  `BuildPlane` and `CheckPlane` hold the executor, and `stop_build_plane` and
  `stop_check_plane` close it with `executor.drain_ms` and
  `executor.helpers_ms`; a close that errs falls back to `exec.stop_pool`
  and reports nothing, so no native-exit verdict is invented. Production
  therefore has one execution model. `broker.start` is the same service
  over a pool's seams, for tests and `client/demo`; `broker/direct` is gone.
- `client/serve.Settings.base_policy` — the base every tool call is
  composed against, and the thing an escalation widens. A field rather
  than a `base_policy(workspace)` call inside `boot`, so a host may serve
  a narrower base and let approvals widen it per call; `main` fills it
  with `base_policy(workspace)`. Under the default nothing can park (see
  Invariants), which is what made the escalation plane unreachable from
  an end-to-end until `client/tui_e2e_test`.
- `client/serve.Settings.home` — the operator's home directory, where the
  global `AGENTS.md` is looked for and carried ahead of the workspace's
  files.
  `resolve` fills it from `HOME` and `None` records that `HOME` was
  unset. A field for the same reason `base_policy` is one: a boot that
  reached into the process environment for it would make every test that
  stands a server up read the developer's own `~/.agents/AGENTS.md`.

## Extensions

`client/extension/*` is the install *and dispatch* halves of the
extension design (`docs/design-notes/extension-architecture.md`,
"Hardening the install" and "Tool registration and dispatch"). Three
rulings shape it: an install fetches a **tree, not a git session**, so
there is no git client anywhere in the path; the tree it fetches is
**untrusted input read outside any jail**, so every reader here is
total; and every tool call is answered by a **jailed satellite**, so
nothing an extension ships ever runs in the harness VM.

Phase 3 changed the third of those from "one jailed execution per call"
to "one jailed satellite per session" (`protocol-change/012`, ADR-007
Decision 3). An extension's artifact is compiled once at install, so a
node boot per call was paying for nothing, and an extension could keep no
state at all between calls. `client/extension/hosts` now holds one
`codemode/satellite.Host` per installed extension for the life of the
session and sends each call to it as a `hook_call`. Holding a node open
widens nothing: the satellite host mints a token per invocation and
revokes it on the answer, so an extension may keep actors between calls
and may not reach a capability between them.

- `client/extension/source` — the grammar of what an operator may type.
  `Source` is `LocalPath | ArchiveUrl | GitHub`; `parse` refuses
  `git://`, `ssh://`, `git+ssh://`, the scp-style `git@host:path`,
  `file://` and `http://` by name, with a message that states the three
  accepted forms. `archive_url` derives codeload's
  `https://codeload.github.com/<owner>/<repo>/tar.gz/<rev>` (`HEAD` when
  no revision was named) and refuses a revision paired with an archive
  URL, because an archive URL already names exactly one tree. `host`
  extracts the lowercase host the fetch policy allows, refusing userinfo
  rather than stripping it.
- `client/extension/archive` — the total tar.gz reader, the local
  directory walker, and the tree digest. `extract` inflates through
  `client/internal/ffi_zlib` under `Caps.max_total_bytes`. Its Erlang adapter
  delegates inflation to the shared `host_zlib_ffi` primitive, abandoning
  the stream the moment it goes over, so a decompression bomb is never
  materialised; the ustar reader then admits only regular files,
  directories and pax headers, refusing links, devices, fifos and GNU
  long-name extensions by name. `from_directory` pushes a real directory
  through the same collector, additionally refuses a symlink (the root
  included — `path` is lstat'd before the walk) and a non-regular file,
  and skips a directory named exactly `.git` so that a working checkout
  reads as its export. `digest` is
  lowercase hex SHA-256 (via `tools/blob.ref_for`, the tree's one
  SHA-256) over a length-prefixed encoding of the sorted files, and is
  independent of `Tree.root` and `Tree.commit` — which is what lets an
  install record be re-verified against a tree staged anywhere.

The exact path subset, and why the space and the invisible formatting
code points are outside it, is stated in `archive`'s module doc.

The rest of the path is phase 1's own, and each module is one question:

- `client/extension/manifest` — the total `extension.toml` decoder.
  `Manifest` carries `[extension]`, the `[[tool]]` list (name,
  description, `prompt_snippet`, `parameters`, `entry`, `timeout_ms`),
  the `[[hook]]` list, and `[net]` with its `[[net.secret]]` bindings.
  Unknown keys are errors in *every* table, which is what refuses the
  `[client]` table the design note reserves for a later ruling without a
  special case for it. `tier` decodes `"jailed"` (`Jailed`) and
  `"profile"` (`Profile`, ADR-016 §3). Three rules need
  the tree beside the manifest, so `decode` takes a `Surroundings`: a
  tool's `parameters` must be a path under `schema/` that exists and
  parses as JSON, its `entry` must name a module `src/` ships, and a
  secret's `host` must be one of `[net].hosts`. A secret carries the
  *name* of an environment variable and never a value, the rule
  `api_key_env` set one layer out. `Manifest` also carries `lsp:
  List(profile.LspServer)` and `checks: List(Check)` (`Check(server,
  fixture, query: CheckQuery(Definition | References), symbol, path:
  Option(String), line: Option(Int), expect: List(Site(path, line)))`).
  **Invariant: the tiers declare disjoint tables, each refusing the
  other's by name.** A profile needs at least one `[lsp.<name>]`, decoded
  by `profile.decode_servers` so a profile means in a manifest what it
  means in `loom.toml`, and refuses `[[tool]]`, `[[hook]]` and `[net]`
  ("a profile extension runs no code; [[tool]] is not allowed"); a jailed
  manifest refuses `[lsp]` and `[[check]]` and still needs a `[[tool]]`.
  A check's `server` is one of the manifest's own, its `fixture`
  (default `fixture`) holds a file in `Surroundings.files`, `line` needs
  `path`, and `expect` is non-empty; every path is relative with no
  empty, `.` or `..` component. `declared_tier(text)` reads the tier
  alone, for the install to choose its path.
- `client/extension/record` — the install record, JSON with a total
  decoder, and the `Root` value that says where installs live.
  `root_for(home)` is `<home>/.loom/extensions`; the root is a value
  resolved from `Settings.home` (or `--home`) rather than an environment
  read inside the pipeline. The record stores the *terms* of the
  approval — the tree digest, the manifest hash, the allowlist, the net
  policy with secret names only, and (format 2) the `#(event, entry)`
  hooks — because recomputing them at load would mean an operator's yes
  silently followed the harness's current idea of the seam. A format-1
  record is refused rather than read with an empty hook list: it cannot
  say whether hooks were approved, and the honest answer is to ask.
  `readable(text)` is the door discovery uses, and it decodes the
  version *first* — the full decoder would otherwise reach a format-1
  file before the version check and report a missing `hooks` field when
  the fact an operator needs is the version skew. Format 3
  (`format_version`) adds `tier` and `lsp`, the approved profiles in full
  through `profile.encode_server` / `profile.server_decoder`; a format-2
  record (`legacy_format_version`) is still read, as `Jailed` with `lsp:
  []`, and every other format is refused naming "reads 2 or 3". A profile
  record's `allowlist`, `manifest_hash` and `artifact` are empty.
- `client/extension/install` — the pipeline, as six steps each returning
  a `Failure` naming its layer: `Fetch`, `Extract`, `Manifest`,
  `Vetting`, `Compile`, `Record`. The fetch and the jailed build are
  both injected (`Fetcher`, `Build`), so the module holds no HTTP client
  and no broker, and a test drives the whole thing with a fetcher that
  was never called. `run` reads the fetched tree's tier first
  (`manifest.declared_tier`) and takes one of two paths. **A profile
  install never calls the build seam and never vets**: it decodes the
  manifest against the fetched tree's text, prunes to `extension.toml`,
  root-level `README*`/`LICENSE*` files (a path with a `/` never matches,
  so `README-assets/` is pruned) and each check's fixture directory, refuses a
  kept non-UTF-8 file, and stages and records exactly as the jailed path
  does, with no artifact directory. The rest of this entry is the jailed
  path.
  **`installed_tree` runs first and everything after sees only what it
  kept.** A repository is not an installed extension — it has tests, a
  `.gitignore`, `.github/`, docs, Gleam's resolved `manifest.toml` and a
  `build/` directory — so `codemode/vet/package.installed_subset` prunes
  it to `src/**/*.gleam`, `schema/**`, `skills/**`, `extension.toml`,
  `gleam.toml`, `README*`, `LICENSE*` before the UTF-8 decode, the
  manifest, the vetting, the digest and `write_tree`. That ordering is
  what makes the recorded digest a claim about the *installed* tree, and
  it is why the UTF-8 refusal reaches only installed files: a screenshot
  under `docs/` is pruned, and one under `schema/` is refused.
  `entry_source(tools, hooks)` generates the `loom_satellite` module that
  imports each registration's entry module and calls
  `ext/runtime.serving(tools:, hooks:)` — a call that runs for the life
  of the satellite rather than serving once. A `[[tool]]`'s entry module
  must expose `pub fn run` (`entry_function`) and a `[[hook]]`'s must
  expose `pub fn on_event` (`hook_function`): two names rather than one
  because a module may serve both, and generated code that named them the
  same would not compile — in the worst place for a compile error, since
  nobody wrote the file. The aliases are positional and per distinct
  module, because two tools may share an entry module, a tool and a hook
  may too, and importing one twice is a compile error in generated code.
- `client/extension/installed` — `discover(root)` and `one(root, name)`,
  each returning `Ready` or `Refused`; `verified(root, name)` makes the
  same checks and answers `Verified(record, manifest, artifact, tree)`,
  the `archive.Tree` the digest was computed over, for a caller that uses
  the installed files (`check` writes its fixture from it). Five things are re-derived from
  disk and compared with the record: the tree digest, the artifact's
  content address (with `build.fingerprint_directory`, the function the
  build itself used), the manifest, the vetting, and the allowlist. After
  the manifest, the manifest's tier must equal the record's (so a record
  cannot talk a jailed tree out of its vetting), and a profile record then
  skips the vetting, the allowlist and the artifact and checks instead
  that the manifest's `lsp` equals the record's; a mismatch refuses "the
  manifest's language profiles no longer match the install record".
  `Ready.artifact` is `""` for a profile. `summarise` prints a profile as
  `lsp: go (.go), rust (.rs)`.
  **Nothing is pruned here**, and that is the point: the install already
  narrowed the repository to the extension's own tree and wrote exactly
  that, so what is under `<name>/src/` *is* the installed tree and a file
  dropped in afterwards must change the digest. Pruning again at load
  would forgive exactly the tampering the digest exists to catch. A
  refusal is a *value* rather than a shorter list, because an operator
  who installed something and sees nothing cannot tell "it is broken"
  from "I imagined it".
- `client/extension/cli` — `install`, `list`, `remove`, `verify`,
  `check`, the first subcommand surface in the tree. The verb is the first argument
  and the rest is the flat-recursion flag parse `client/serve` uses.
  `build_for` is the install's build seam over a started `BuildPlane`.
  `install` starts that plane **inside** the build seam, on the one call a
  jailed install makes, and stops it there on every path out, so a
  profile installs on a host with no code-mode toolchain or helper; a
  plane that will not start is a `compile:` refusal. `installed_lines`
  renders a success: tools and the jail's enforcement line for a jailed
  extension, `profile.approval_lines` per server for a profile. `check`
  is `client/extension/check.run` with the operator's demand
  (`--best-effort` as `install` spells it) and the daemon's places and
  environment; a failed check is an exit-1 error whose text is the whole
  report.
- `client/extension/check.{Setup, Run, Report, run, lines, failed, total,
  enforcement_line, release_wait_ms}` — `loom ext check` (ADR-016 §5).
  `run(root, name, setup)` refuses, before starting anything, an
  extension `installed.verified` refuses, a jailed one, and a profile
  with no `[[check]]`; then per `(server, fixture)` group it writes the
  fixture from the verified tree's bytes (never copied from disk, which
  would follow a link planted after install) to
  `<root>/.staging/check-<token>/work`, judges the scratch's real path
  (`host/bootstrap.canonical_directory`) against `/tmp` and
  `/private/tmp` and refuses either by name, starts `serve.start_check_plane` over it, prints the
  probe's `enforcement.Report` (`manager.probe_server`), starts a
  `manager` over the one approved server (roots via
  `workspace_policy.lsp_server_roots` and `Setup.places`, `toolchain: None`), runs
  `profile_check.run_all` through `manager.door`, and stops the manager,
  waits for its lease, aborts the operation, stops the plane and deletes
  the scratch on every path out. **Invariant: the profile proved is the
  install record's**, the one the operator approved, and the fixture is a
  copy, so a writable project never edits the digest-guarded tree.

Phase 3 added the hook bus, and it hangs off the same satellites the
tools reach:

- `client/extension/hooks` — two `weft/event_manager`s per session, one
  handler per installed extension that declares a `[[hook]]` on each, the
  handler's state its name, the events it declared and its `Invoker`.
  Two managers because a manager's mailbox is a queue: the four
  notify-only events are cast onto the notice manager and the three
  answering ones are `sync_notify`ed on the other, so a slow `usage`
  handler cannot spend the `tool_call` gate's fan-out budget and turn a
  second extension's block into an `Allow`. `subscribers(bus, on:
  Answering | Notifying)` is per manager for the same reason a drop is:
  `event_manager` removes a handler from the inside and offers no handle
  onto its twin.
  `Invoker = fn(String, String, MsgPackValue, Int) -> Result(MsgPackValue,
  HookFailure)` is the seam onto the persistent satellite host, injected
  so the bus is drivable with functions and so the host lands in one
  place; `HookFailure` is `Unhandled | Refused(reason) | Crashed(reason)
  | Deadline | Gone`, of which the last three cost a handler its place
  and the first two do not. The one implementation a session builds is
  `hosts.invoker(hosts, at:)`, so a hook event and a tool call reach the
  same node; `unwired()` — every call `Gone` — is now only what a test
  drives the bus with. Seven events fan out (`session_start`,
  `before_agent_start`, `tool_call`, `agent_end`, `agent_settled`,
  `before_compact`, `usage`), the three that need an answer through
  `sync_notify` plus a drained reply subject — on a weft-bounded worker, never on the caller, because
  `sync_notify` is a `call` and a `call` that is not answered in time
  panics its caller, and the callers are strand drivers sharing one
  answering manager. An unanswered fan-out is an empty list: no injection, and no
  block on a call the built-in clearance already cleared. `context` and
  `tool_result` are chained transforms and so are
  `fold_context`/`fold_tool_result` over the same ordered list, not bus
  events. `wire(effects, bus, session, clock)` composes the bus into a
  built `Effects` by *wrapping* seven slots, the pattern
  `notes.digest_hooks` and `agency.reaping_hooks` set. `injection` and
  `note_block` are the `<extension name=…>` fences a run-start injection
  and a compaction note are rendered in — the harness writes them, never
  the extension, for the reason `system_prompt.render_file` gives about
  instruction files. The module documentation is the normative table of
  all nine wire shapes; the extension's side of the same wire is
  `ext/hook`.
- `client/extension/hooks.{compaction_notes, usage}` — phase 4c's two
  events. `compaction_notes(bus, op, cue)` fans `before_compact` out and
  gathers every note in load order, rendered through `note_block` and
  bounded cumulatively by `context_growth_tokens`; a note that would
  overrun the allowance is dropped whole rather than truncated, because
  half a note says something its author did not. It rides on
  `effects.Hooks.compaction_note` and it is **not a veto** — the
  compaction is already decided when the event fires, which is the
  non-vetoing form of the `session_before_*` hooks the design note
  refused. `usage(bus, op, row)` is a plain `notify` on
  `effects.Hooks.usage`, carrying the ledger row's numbers and
  coordinates and never its `details`.

Phase 2 added the dispatch half and phase 3 the session's hosts, as
four modules — three pure or stateless, plus the one actor that owns
the nodes:

- `client/extension/policy` — pure. `egress_for(net, trust:)` turns the
  manifest's `[net]` table into `Egress`: `Reaches(egress.Policy)` or
  `ReachesNothing` for a manifest that named no table. `hosts`,
  `methods`, `max_response_bytes` and the `[[net.secret]]` bindings are
  the manifest's verbatim; `redirects` (`SameHost(2)`), `timeout_ms` and
  `trust` are this module's, because none of the three is something an
  extension author should be able to state about themselves.
  `ceilings(net)` is `requests_per_call` on `net.request` and nothing
  else — the `ext.call` ceiling went with the capability
  (`protocol-change/012`), and the tally is now reset per *invocation* by
  `satellite.Invoking`, which is what makes `requests_per_call` mean per
  call on a node that outlives the call. `denial(refusal)` sorts every `egress.Refusal` onto the
  side of `cap/net.map_error`'s split it belongs on — a request the
  policy would never have permitted carries a `NetDenied` code
  (`not_allowed`, or `network_off` for no `[net]` at all, or `policy`
  for the ceiling), and a permitted request that failed on the wire
  carries `net_failed`, which is `NetFailed`. Filing one on the wrong
  side is the difference between an author retrying and an author
  reinstalling.
- `client/extension/seam` — the router arm, msgpack in and msgpack out,
  holding no policy at all. `routing(extension, over: inner)` answers
  `net.request` (through `Egress.perform`, an injected function with the
  policy, the credential lookup and the refusal mapping closed over) and
  the two memory arms, `ext.remember` and `ext.recall` (through
  `Memory`'s two closures, with the durability behind them), and hands
  every other name down. `serviced_caps` is those three names; the
  `ext.call` arm with its `Call`/`call_value` shapes went with the
  capability. The memory arms check the *leaf* key they are sent —
  `checked_key`: non-empty, no `/`, at most `max_key_length` — and the
  value against `max_value_bytes`, and nothing more: which subtree a cell
  lands in is bound by `client/extension/dispatch` from the install
  record, so this module never learns an extension's name. `Ask`/`Answer` restate `cap/net`'s
  `Request`/`Response` rather than naming `broker/egress`'s, so a test
  drives the whole arm with a function and no socket. Every inbound field
  is decoded totally; a malformed request is refused by the *plan*, so it
  consumes no ordinal and no admission.
- `client/extension/memory` — the durable half of those two arms, and
  the same split `client/scheduleseam` fills for `tools/schedule`: the
  seam owns the wire, this owns the store. `Cell(extension:, key:)` and
  `key(cell)` are the one composition of `ext/<name>/<key>`, so the
  confinement is a line rather than a convention; `door(Wiring(runtime:))`
  borrows the live runtime through the Agency's holder
  (`for_session(agency_config)`, because the runtime does not exist until
  `api.open` has returned the registry these tools are in);
  `shut(reason)` is what a host with no session hands the dispatch, so
  the capability is routed and refuses with a sentence. A write is
  `put_reserved_fact` — blind, latest-wins, no compare-and-set — and a
  read is the plain `fact`, which never consulted the reservation. The
  value is parsed with `core/json.parse` on the way in and rendered on
  the way out, so a cell holds a document rather than a quoted string.
- `client/extension/hosts` — the session's satellites, held open and
  handed out. One supervised actor per session keeps at most one
  `satellite.Host` per installed extension, starts it lazily on that
  extension's first use, and stops every one of them when the session
  ends. `invoke` and `invoke_event` are the two entry points, over a
  `Hosts` seam closed over the actor's registered *name* rather than a
  subject (the `client/scratch.seam` pattern: the seam is built while the
  registry is assembled and the actor starts later, under a supervisor);
  `none()` is the seam a session with no registry hands out, refusing
  every invocation in band. `Coordinates` is where an invocation sits —
  `{op_id, step_id, strand, workspace, base_policy, demand, env}` — one
  record because a tool dispatch fills it from `tool.Ctx` and a hook fills
  it from the strand the event fired on, and a positional signature they
  both have to get right is one either can get wrong silently.
  `Extension` is the recipe: a name plus `start`/`invoking` *functions*,
  because a host is launched under the coordinates of whichever call was
  first and an invocation is judged under its own. `HookFailure`
  (`Unhandled | Refused | Crashed | Deadline | Gone`) is deliberately
  smaller than `satellite.InvokeError`: what a caller does depends on
  whether the extension answered, died mid-answer, ran out of time, or is
  not there any more. **The actor performs each invocation on its own
  timeline, so its mailbox is the queue** and a second caller waits rather
  than reading `Busy` — which makes it a session-wide serialiser rather
  than a per-extension one, a deliberate simplification whose only cost is
  two *different* extensions invoked from two strands at the same moment.
  A host the satellite lost is `Departed` for the rest of the session, and
  every later call on that extension is `Gone`.
  `invoker(hosts, at:)` is `invoke_event` curried into a
  `hooks.Invoker`, with `bus_failure` restating this module's
  `HookFailure` in the bus's; it is what makes a hook and a tool call
  land on the same node. It satisfies the bus's contract by
  construction rather than by care: the host arms the invocation's
  deadline as the state timeout of the `Answering` state it belongs to,
  and every step of `invoke` is a `Result`, so a wedged registry
  degrades to `Gone` where a `process.call` would have exited the strand
  driver. `client/serve` builds one such invoker per session, under
  coordinates it mints an operation for: the bus's `Invoker` carries
  none, because a hook fires on the harness's own timeline rather than
  inside a run whose `{op_id, step_id}` it could borrow, so a hook's
  reads are attributable to the session's hooks and never to whichever
  run was in flight.
- `client/extension/dispatch` — `tools(config, record, manifest,
  sources:, artifact:)` turns one `installed.Ready` into `tool.Tool`
  values (`prompt_snippet` from the manifest, schema read from the
  installed `schema/` file, `replay: tool.Never`, `execution_mode:
  tool.Exclusive` — neither declared by the manifest, because both are
  judgements about what the harness may do with a call), and `hosting`
  turns the *same* record and the *same* configuration into the
  `hosts.Extension` the registry launches from, in the same call, so a
  host and a tool cannot end up describing different extensions.
  `Config.hosts` is the registry seam. Each `run` builds this
  invocation's `coordinates(ctx)` and hands them to `hosts.invoke` with
  the model's arguments as JSON text in the `{args, strand}` envelope
  `ext/runtime` reads; the answer goes through `settle`, which now takes a
  `Result(MsgPackValue, hosts.HookFailure)` and is still public because it
  is the one part of a dispatch a test can hold still. **No work directory
  is prepared or removed per call**: a host's token file lives under
  `codemode.host_root(config, extension:)` and its socket under
  `codemode.host_socket_directory(config, extension:)`, both keyed on the
  extension because they outlive every call. The router is two layers now rather
  than three — `seam.routing` over `codemode.workspace_seam_for`'s bridge
  over `satellite.default_router` — and it, the ceilings and the clearance
  identity are all built per invocation. A call's wall budget is `within`,
  the manifest's `timeout_ms` clamped to the operator's `max_within_ms` —
  an extension tool is `Exclusive`, so the call holds the strand's
  exclusive slot for the whole of it, and an install is not a way to raise
  a host's ceiling. The *node's* own budget deadline is
  `host_lifetime_ms` (twelve hours): the backstop a runaway satellite dies
  against, far longer than a session and far shorter than forever, and
  nothing anybody tunes. There is no MCP arm: `cap/mcp` is on no seam, so
  an extension cannot name it. `Ctx.grants` are **not** composed onto an
  invocation — an extension runs at exactly what its install approved,
  where a `code_mode` call is the model's own program in this turn and
  does compose them.

**The credential path, stated once.** A `[[net.secret]]` names an
environment variable, a host and a header. The *name* travels into
`egress.Policy`; the value is read by `broker/egress` through the
`secrets` function `serve` injects (`env_text`, the same store
`api_key_env` reads), after the origin and the method are judged, and
goes straight on the matching hop. It is in no `Tool`, no `seam.Answer`,
no `egress.Refusal` (no variant has a field for one), no `node_env` and
no `env_allow`, and `policy.summary` counts bindings rather than naming
values. `client/extension_e2e_test` reads the `LaunchSpec` and taps every
frame in both directions and asserts the value appears in neither.

**Boot registration is in `serve.assemble`.** `extension_registrations`
reads `installed.discover(record.root_for(Settings.home))`, logs each
`Refused` under `extension.refused` and retains its bounded operator notice,
 logs `extension.unavailable` for a
`Ready` on a host with no toolchain — no `erl` means no satellite, and
registering tools that can only fail would put them in the provider's
cached byte prefix — and returns *both* halves of one discovery: the
`contributions.Contribution` per extension, appended after the built-ins,
and the `hosts.Extension` recipe the satellite registry launches from.
One pass rather than two, because deriving them separately is how a host
and a tool come to describe different extensions. A repeated name refuses
the boot in `contributions.registry`.

**The satellite registry follows the scratch store's two-name pattern.**
`assemble` mints `loom_ext_hosts` before the tool registry is built, hands
`extension_hosts.seam(name, margin_ms:)` to the dispatch configuration,
and adds `extension_hosts.supervised(name, extensions.hosting)` to the
*services* supervisor — that tier because a registry restart costs exactly
what a satellite crash costs, which extensions are already written to
meet: every host it held is `Gone` to its next caller, the tools stay
registered, and each lost node is reaped by the launcher's own janitor.
`extension_host_margin_ms` (30 s) is the slack a caller's wait has over
the invocation's own deadline, so a caller never reports a wedged registry
for an invocation that was merely being timed out properly.

**The TCB freeze is a test in this package.**
`client/test/client/extension/freeze_test.gleam` is #33's record in code:
the compile-time half walks the package graph (`packages/ext` names only
`cap`, `cap` only `core`, `core` none), the resolved `manifest.toml`,
`codemode/seed.default_vendored`, and every import in `packages/ext/src`
and `packages/cap/src`; the runtime half pins `policy.extension()` and
`policy.resident()` as exact sets and shows both disjoint from every
module the TCB packages ship, walked from the tree so a new module under
`storage/` is covered the day it lands. It lives here rather than in
`codemode` because only this package can see both the seams and the tree
without a new dependency. The loader (#32) is deferred;
`docs/review/extension-zone.md` says why and carries the measurement a
loader would inherit.

**The verb split lives in `client.gleam`, not `serve.main`.** The
installer needs the boot's own effect plane, so `extension/cli` imports
`client/serve`; putting the dispatch in `serve` would be an import cycle.
`client.gleam`'s module doc says so, so the next reader does not move it
back.

**`serve.start_effect_plane` and `serve.start_build_plane` are the boot's
own, factored out.** An install runs the same jailed, network-off `gleam
build` a code-mode program is built by, against the same seed, under the
same base policy, found by the same helper and seed ladders. Two
implementations would be two answers to "may this build run". The
`writable` and `workspace` arguments are separate questions and a boot
only ever asks them of one directory: the seed ladder looks in the
checkout, and the jail may write only where the build root is, which for
an install is under the extensions root. `state_root` is the third: the
daemon's credentials sit one directory above the extensions root and the
build plane masks them where the jail can build the mask.
`serve.start_check_plane` is the third caller's shape: the same helper
ladder and effect plane over `build_plane_policy(workspace, state_root)`,
with no toolchain discovered, for `loom ext check`; `lsp_places` and
`lsp_server_roots` are public so the check expands a profile's roots
exactly as a session does.

## Imported hooks

The Claude-compatible hook layer (issue #350) is six modules, one
question each, and one composition point in `serve`:

- `client/hookcompat` — the model both accepted formats decode into: the
  Claude settings JSON verbatim and the native `[[hooks.Event]]` TOML.
  Both parsers are total with worded errors; unknown handler *fields* are
  ignored (Claude ignores them), an unknown event or handler type is a
  refusal. Two entry points for the Claude shape, because the caller is
  the one who knows which document it located: `parse_claude` reads a
  **bare hooks object** (a plugin's `hooks/hooks.json`), and
  `parse_claude_settings` reads a **whole settings file**, descending its
  `hooks` key and returning an empty `Config` when there is none. Choosing
  between them by guessing at the absent key read `permissions` as an
  event name and refused the ordinary user settings file. `merge`
  concatenates in the caller's precedence order and never replaces;
  `to_toml` renders the native shape losslessly (no CLI exposes it yet);
  `hash` digests the model canonically for the trust record and ignores
  the `Source` it was read from; `notes` reports unsupported kinds and
  no-moment events at load time rather than loading them silently.
- `client/hookrunner` — one imported command hook as one jailed
  process through `broker.clear_call`, the worktree observation's own
  pattern: `sh -c` shell form or `args` exec form, the event JSON on stdin
  closed after, the handler's `timeout` seconds as the wall limit and
  the budget deadline, a timed-out run reporting `WallCancelled` with no
  output for the caller to read a decision out of. `Outcome.capture`
  (`Whole | Clipped`) is the same story for the output cap: a stream cut
  at the cap is blanked exactly as a cancelled one is, so a half-written
  object cannot classify as a clean decision. The environment is the
  session's, composed by `serve.hook_environment`; the cwd is
  the workspace, and the jail is the session's base policy — never a
  grant the hook could widen. **The requirement is the keys of that
  environment under `RefuseNarrowed`**, so every name it carries has to
  be on the session base's allowlist or the whole call is refused before
  a process exists; `workspace_policy.allowing_imported_hook_env` is what grants
  `CLAUDE_PROJECT_DIR` there, beside `allowing_tool_tmpdir`'s `TMPDIR`.
  Nothing rewrites the command string: `Context.env` carries `HOME` and
  the shell expands `~` against it, so the substitution
  `serve.hook_environment` makes is the whole of the tilde story. `HOME`
  is deliberately not a field of `Context` — `call_spec` derives the
  `env_allow` requirement from the keys of `env`, and a second copy
  could disagree with the one the process actually runs with.
- `client/serve.hook_environment(List(#(String, String)),
  Option(String), String)` — the environment one imported hook runs
  under: the session's, with `HOME` replaced by the operator's home when
  there is one, plus `CLAUDE_PROJECT_DIR` naming the workspace. A jailed
  *tool* keeps the workspace-local `tool_home_directory` so what a
  toolchain writes to `$HOME` stays off the operator's tree; a hook
  cannot, because an imported collection names scripts at
  `~/.claude/hooks/...`. `None` — a daemon started with `HOME` unset —
  leaves the jail home standing.
- `client/hookdecisions` — a finished outcome read back as the decision
  the pinned contract (`docs/design-notes/claude-hooks-contract.md`)
  specifies, per event: exit 2's block, the `hookSpecificOutput` field
  set guarded by its `hookEventName`, plain-stdout context on the four
  context-carrying events, and the 10,000-character cap.
- `client/hookwire` — the event boundary: the matcher classification
  (exact lists versus unanchored regex by the contract's character
  rule), the payload's common fields, the Loom↔Claude tool-name mapping
  (`bash`→`Bash`, `fs_write`→`Write`, …) applied to matching only, and
  the verdict combination rules — first deny wins, a rewrite survives
  only when nothing harder landed. `matching_handlers` keeps only
  `Command` handlers, which is what makes "parsed, not run" structural
  for the other four kinds rather than a promise each gate has to keep.
- `client/hookserve` — source loading with per-source trust (`client/hooktrust`)
  and the composed gates: `tool_gate` at the clearance, `tool_feedback` at
  the result fold, `session_context` at run start, `compaction_note` at
  the summarizer, `stop_gate` at the run-end boundary. `Located.shape`
  (`ClaudeSettings | ClaudeHooks | LoomToml`) is what picks the parser.
  `Serving` carries the merged configuration **only inside `wiring`** —
  a second copy diverged from it and every gate matched nothing — plus
  `skipped: List(Skipped)` (path and the reason, so the boot log can
  say which of "unreadable", "will not parse" and "not trusted" it was)
  and `notes` for `hookcompat.notes`' findings. `wire` wraps a session's
  `Effects` the way the native extension bus's `wire` does, so both
  layers coexist; one small `weft/actor` holds both pieces of state the
  wrapped slots need — the Stop gate's per-operation continuation tally
  and the `FirstRun | LaterRun` flag that keeps `SessionStart` to once
  per composed `Effects` — because those slots are plain functions that
  can keep no state of their own. The Stop gate is asked only when
  `built.run_end` placed nothing, which is why that `case` is nested:
  Gleam evaluates both subjects of a two-subject `case` before matching.
  The composed `clear` slot takes the wrapped `clear` **function**, not
  its answer, because a `PreToolUse` `Rewrite` has to re-enter it: every
  upstream gate answered about the arguments the model sent, so
  `recleared` puts the replacement back through the harness's clearance
  and returns that second clearance's whole answer, `effective_arguments`
  included: a wrapping layer that normalizes or fills a default hands
  back arguments other than the ones it was asked about, and returning
  the hook's own literal would have kept that layer's work on the first
  pass and dropped it on the second. The hooks are not asked a second
  time — a gate that could rewrite the input of its own next ask has no
  fixed point. The messages the gates place carry the placing event's
  own name (`hook_message`), so a `SessionStart` injection reads as one
  rather than as a stop that never happened.
- `client/hooktrust` — the recorded, hash-pinned trust a non-managed
  source needs before it runs: one JSON record per source under
  `<home>/hooktrust`, so a changed file re-enters review. The operator's
  user-level settings are trusted on **first** sight and re-enter review
  once their hooks change; a project, local or plugin source asks first,
  and nothing in the tree can answer yet — `loom hooks trust` is
  follow-up work, so those sources stay skipped. A server with no home
  has nowhere to keep a record and therefore trusts no imported source.
  The pin covers the declaration, not the scripts the commands name.

`serve.with_imported_hooks` is the one composition point, called after
`with_extension_hooks` so an imported hook's verdict lands after the
native bus's and the harness's own clearance — one authority story.
A session with no imported sources composes nothing. The parity
matrix is `docs/architecture/hooks-compat.md`, whose "What is verified
where" says which layer each claim is tested at; the fixtures are
`docs/fixtures/hooks-compat/`. `hookserve_e2e_test` is the acceptance
case: a real `serve.open_instance` under a temporary `Settings.home`,
an unedited user-level collection, and all four composed events firing
across one operation a `Stop` block holds open.

## Relationships

The `sqlight` module is supplied by the `sqlight_loom` Hex package, whose
`esqlite_loom` dependency carries the native query-retirement repair.
The module API is unchanged; the upstream packages must not coexist with
these forks because they define the same modules.

- **Depends on**: `host` (shared daemon OS bootstrap and WebSocket transport),
  `core` (json, codec, entries, messages), `session`,
  `runtime` (`api`, `effects`, `escalation`, `supervisor`, `writer`),
  `events` (the bus as a hint source), `storage` (catch-up scans),
  `machine` (`acceptance`, `queue`, `codec` — structural preparation and
  typed durable state), `broker` (`policy.Grant`,
  `escalation` vocabulary), `provider` (`stream.Delta` for the tap),
  `prompt` (the system pack, its decoder and renderer),
  `codemode` (the vetting policy, the hermetic compile service, the
  production builder and launcher, and the satellite host — the pipeline
  `client/codemode` fills the `tools/codemode` seam with),
  `mcp` (the JSON-RPC codecs, the stdio client actor, the façade
  generator and the value interchange `client/mcp` wires into code mode),
  `telemetry` (the JSON handler this entry point installs, and the
  logger it injects into `api.Options`),
  `mist` + `gleam_http` (the websocket transport), `simplifile` (the
  token file, the pack file, and the session's instruction files),
  `weft` (the bounded concurrent run `client/mcp.start` fans server
  bring-up out over, native subscription witnessed tasks and custody ledgers,
  and per-profile lock polling), `gleam_crypto` (PKCE entropy and constant-time
  state/nonce comparisons), and `gose` (maintained JWT/JWK signature and claim
  validation).
- The spec DAG (§0.1) writes `L → A,C,E,K`. The `B`, `D`, `F`, and `G`
  edges are real and load-bearing — catch-up scans storage directly,
  compaction and navigation build `machine/acceptance` plans, the delta
  tap types against `provider/stream`, and grants are broker policy
  values — and are worth knowing about rather than papering over. The
  wiring promotion added `tools` (the registry and per-call `Ctx`);
  `client/serve` added `argv` (flags); `client/catalog` added `tom`
  (the pure-Gleam TOML parser behind `--config`); `client/system_prompt`
  added `prompt`, whose purity is why the I/O had to live on this side;
  `client/schedule` added `gleam_time` as a direct dependency — already
  resolved transitively through `tom`, and its total
  `gleam/time/timestamp.parse_rfc3339` is what a one-shot schedule's
  `at` field parses with, rather than a second RFC3339 parser or TOML's
  own bare datetime literal.
- **Depended on by**: `conformance`, whose wiring and e2e suites import
  `client/wiring` (legal — T depends on all). `packages/tui` is its
  native client, coupled only through the protocol and the golden fixtures.
- **`client/internal/timebase`** is the one non-FFI module under
  `internal/`: it adapts a `core/clock.Clock` and a seam's injected `rest`
  into the `weft/poll.Clock` the two foreground waits run on
  (`escalate.park`, `agency.wait_loop`). Its module doc carries the ruling
  a reader will otherwise re-derive — the successor clock `clock.read`
  hands back is discarded, because `from_function` returns itself and
  re-calls the injected function on every read, and `clock.stepping`
  cannot serve a loop that holds one clock value across many reads. A wait
  built on a clock whose `now` does not move never expires; every fixture
  in this tree that drives one of these waits is a `from_function` over a
  counter actor for that reason.
- **FFI**: `client/internal/ffi_os` over `client_ffi.erl`, serve-only:
  wall clock, unique entropy, `PATH` lookup, `code:root_dir/0` and the
  running ERTS version (the anchor `client/install` builds every
  in-tree path from), `os:type`/machine
  architecture for the prompt's platform line, the SIGTERM relay,
  `sys:terminate/3` for stopping the service supervisor the way OTP
  stops one, and the documented exit-code halt. Test-side, `client_test_ffi.erl` is a
  minimal websocket probe for the boot smoke.
- **Native subscription effects** reuse `host/bootstrap` private bounded
  reads, atomic writes and kernel launch locks, `provider/http` native HTTP
  ownership, and `client/daemon/listener`'s parked Mist listener. No custom
  subscription FFI or authentication helper is needed by `client/codex`.
  The pinned Weft revision starts cancelling adopted owners when a worker
  exits unreported, which the transport's custody tests rely on.

## Traffic

- `serve` injects `codex_transport.transport()` into assembled catalogue
  gateways. `gateway.CodexTransport.prepare_streaming` requests a parked
  `http.PreparedRequest`; the gateway publishes its owner before `begin`.
  Inside that operation, `profile.auth(profile, ledger)` acquires the durable
  credential lock, obtains an authorized bearer, and releases the lock before
  the fixed public `/v1/responses` request is prepared. The transport adopts the
  prepared native HTTP owner into the same ledger before starting it.
  `http.ResponseStatus`, `ResponseChunk`, and `ResponseEnd` reach the existing
  provider stream pump. A fixed local authentication refusal becomes a
  redacted 401 response; no saved token or account subject enters those events.
- `codex/transport.command` runs `profile.execute` with
  `codex/profile_control.Command` and a `Subject(ControlEvent)`. Browser login adopts
  `daemon/listener.Listener`, opens ephemeral literal loopback, and emits
  `LoginInstructions` only after the bound port is known. The attempt worker
  checks each request's Host against that actual authority before issuing the
  private redirect or consuming the state-checked callback. The ten-minute
  browser budget is absolute. The issued client registration is saved with the
  grant only after the code exchange and identity verification succeed, and
  that save precedes `LoginComplete`.
  Model discovery decodes the subscription endpoint's `models[].slug` and
  `visibility` fields, validates the whole bounded catalogue, and projects only
  listed identifiers in server order. Model instructions and unknown metadata
  never enter the operator event; the Platform API's `data[].id` is not this
  endpoint's subscription response.
  `Models` calls authenticated public `/v1/models`, bounds its body, and emits
  only validated IDs through `ModelCatalogue`.
- `Logout` attempts refresh-token revocation, clears the grant locally, and
  retains issued client, host and verified subject. An unconfirmed upstream
  revocation produces `LogoutRevocationUnconfirmed` rather than retaining local
  tokens. It does not retract a request already admitted to HTTP. Temporary
  refresh failures preserve the durable grant; terminal refresh failure clears
  tokens while preserving registration. Each later operation rereads disk.

- `protocol.ListSkills(offset)` is a subscribed read available to observers.
  `SkillsSnapshot(board)` returns bounded consecutive metadata rows with a next
  cursor. Explicit slash text still uses existing prompt/content/steer/follow-up
  commands. Expansion precedes held-input custody, so later file changes and
  queue draining cannot replace the captured instructions or original author.

- **Actor messages**: `gateway.Message` — `Attach(sink, reply)` (a call,
  returning the connection id), `Detach(connection)`,
  `FromClient(connection, text)`, `CommitHint`, `BusHint(published)`, and
  `ProviderDelta(operation, generation, delta)` and
  `ProviderEnded(operation, generation)` (casts). Senders: `client/server`'s
  socket handlers, `commit_forwarder` (from `runtime/writer.Event`),
  `events/bus` subscriptions, and `tap_provider`'s wrapper. The hub's bus
  subscription is keyed by the session's **canonical id**
  (`bus.key(of: api.session_id(runtime))`), never by the caller-supplied
  display name (`protocol-change/008`). Under network delivery the hub
  joins the `Outputs` topic alone and turns each `ToolOutput` into a
  pushed `tool_output` frame without pulling, and each `BlockSummary` into
  a pushed `block_summary` frame the same way (protocol 050); every other
  `BusHint` is a pull. The host fixture joins every topic.
- `blocksummary.Message` (opaque) — `Streamed(operation, generation,
  chunk)` and `StreamEnded(generation)` (casts from the provider relay's
  observer process, per reasoning fragment and per stream end),
  `Committed(seqs)` (mapped from the `writer.Event` the writer sends to
  the machine's own commits address) and `Relayed(flight, pulled)` (a
  request's relay, on its per-flight sink). A lost cast costs one live
  summary, which the stream's next growth offers again; a lost hint is
  covered by the next hint's scan from the high-water.
- `gateway.DrainHeld(flushed, reply)` fences mutations, returns held items,
  queues each transport's flush marker, and replies with the acknowledgement
  count. `daemon/session_socket.Flush` follows `Push` from the same gateway
  sender and acknowledges completed socket writes. The drain task receives
  these acknowledgements outside the gateway, within its remaining budget;
  a socket can still finish a request against that gateway while flushing.
- `advisor.Message` — `PrimaryRunEnded(operation)` and
  `AdvisorRunEnded(operation)` (casts, from the wrapped `run_end` slot on
  the strand driver's own process), `PrimaryStepped(operation)` (a cast,
  from the wrapped `usage` slot on the same process, one per committed
  provider request of the primary's), `Judge(strand, verdict, reply)` (a
  call, from the `advise` tool's effect process, bounded at
  `judge_timeout_ms`), `TakePending(operation:, reply:)` (a call, from the
  wrapped `run_start` slot, bounded at `pending_timeout_ms`) and
  `TakeAtRunEnd(deadline:, reply:)` (a call, from the wrapped `run_end`
  slot, the same bound, carrying that bound as a wall-clock instant so a
  request served after the hook gave up answers with nothing rather than
  spending the turn into an ended run). `TakePending` carries the operation being opened because
  the actor reads it as well as answering: a run start whose operation is
  not the one this actor itself last woke is where this operator turn's
  one unsolicited-delivery budget is handed back. `TakeAtRunEnd` carries
  no operation, because it decides nothing from one — the hook has
  already established whose run is ending, and it is where that budget is
  *spent* rather than renewed, and only once per turn: a second ask
  inside the same turn answers with no nudges without touching the guard.
  All three calls go through a monitored send-and-select rather than
  `process.call`, so an absent or wedged actor degrades to no nudges and
  an in-band refusal instead of killing a strand driver or a live tool
  effect. All three notifications are casts on purpose: a driver that
  waited on a branch scan and a provider round trip would stop serving
  `Nudge`, `RequestAbort` and `PollTick` for the length of a review. A
  lost step cast costs a threshold reached one step later, never one not
  reached.
- `glance.Message` (opaque) — `Stepped(operation, context)` (a cast, from
  the wrapped `usage` slot on every strand driver's own process, one per
  committed provider response), `Wake` (the machine's own named timeout)
  and `Relayed(strand, pulled)` (a request's relay, on the per-request
  sink the machine selects while that flight is booked: the task's
  `PulledOutcome`, then `AllDelivered` or `RunLost`). The step is a cast
  because a strand's run must never wait on a label. A lost cast costs one
  refresh, which the strand's next step offers again.
- `history.Message` — `Pull` (a cast: a commit landed, go sync),
  `Synchronize(reply)` (a call, for a test or an operator), `Query(text,
  limit, scope, reply)` (a call, from the tool seam), and `Stop`.
  Senders: `commit_pull`, the writer's *second* subscriber, which is what
  makes recall a commit-driven projection rather than a scheduled sweep;
  and `client/history.seam`, from a live tool call's own effect process.
  Both reach the holder by name, and both degrade an absent or wedged
  holder to an in-band refusal rather than to a dead caller.
- **Commits**: the hub commits nothing of its own except through the
  session's one writer. Commands map onto `runtime/api`
  (text or ordered-content prompt/steer/follow-up/abort, escalation approve/deny, strand
  creation, compaction and navigation). `api.compact` and `api.navigate`
  build the `machine/acceptance` plans and commit through `runtime/writer`,
  sharing admission with every other runtime caller. Nothing bypasses the writer. An absent runtime writer is a typed
  `api.RuntimeUnavailable`, rendered as a retryable `conflict` by the gateway;
  the rule and schedule scanners hold the attempted admission for a later
  scan. This is distinct from a lost reply, whose commit may already exist.
- **Registers**: reads `strand.*` (configuration, leaf, state, last
  result) and `op.meta`/`op.state` through the session's typed accessors
  to build snapshots and detect terminals; reads the runtime's escalation
  records under `fact.custom`'s reserved `escalation/` prefix, and the
  assembled system prompt and its enforcement identity under the reserved
  `prompt/` prefix — read off the session store directly before `api.open`,
  written back through `api.put_reserved_fact` after it. The rule scanner
  owns the reserved
  `rule/` prefix: `rule/fired/<strand>/<name>` write-once marks, committed
  with the injection they authorize, and `rule/cursor/<strand>`
  checkpoints, written lazily. Every one of the scanner's *reads* goes
  straight to the session store, never through the writer's mailbox, so
  a scan can never sit in front of a settlement; the fire itself is an
  ordinary queue admission through the writer, on the scanner's own
  process. Protocol `fork` and `create_strand` seed idle strands through
  `api.create_idle_strand`, whose CAS-guarded commit writes `strand.config`,
  `strand.leaf` and `strand.state` without admitting a task brief.
- **Search index**: `client/history` writes only to the repository's own
  `loom-search.db`, beside the session file and never inside it, through
  `events/search.sync`. Index rows and the advanced cursor commit in one
  transaction, so a crash mid-batch re-runs the batch into the same
  state; a lost poke costs latency and never a row.
- **Memory**: the boot's own probe still opens nothing — it reads the
  store's header lease-free (`storage/sqlite.generation`), and an absent
  store is the ordinary state of a repository that has never remembered
  anything — but the *server* now writes the store, through the
  supervised `client/distillpass` worker every ordinary boot starts
  (#149). It reads `loom-memory.digest` at **every run start** through
  `memory.digest_hooks`, which takes a thunk rather than bytes for
  exactly that reason: this host runs the producer as well, and a digest
  captured at boot would hold every session one pass behind its own
  pipeline. Memory therefore lands on *run* boundaries rather than
  session boundaries, which costs the same rolling tail write it always
  did — the digest rides messages, never the pinned head — and nothing
  can touch a run already open. The writers are `client/distill` (the
  pipeline, holding the memory session's writer lease for its whole run
  under `run_lease_ttl_ms` — or under the short one for a `--cascade`
  pass, which has no model turn between its commits), reached either from
  the lifecycle worker or from the command line, and the `remember` seam
  (one open per call under the short `lease_ttl_ms`, refused in band
  while a run holds the lease). Usage rows for both model turns land in
  the memory session's own ledger.
- **Wire**: JSON text frames over websocket. The envelope `seq` **is the
  storage seq** of the write that produced the event, so the durable
  stream needs no side index and `catch_up` rebuilds it with
  `scan_entries` / `scan_usage` plus register reads.

- The `conformance/simulation/daemon` harness and scenario runners are
  sanctioned callers of `@internal` `daemon/root` and `daemon/manager`
  operations. They drive startup, lifecycle, authority and domain retirement
  through the real registry; a rename here must update those callers.

## Invariants

- Markdown skills are assembled from the daemon's configured home, not a
  terminal's filesystem. Metadata reads never disclose bodies. Automatic tool
  dispatch honors `disable-model-invocation`, while slash invocation honors
  `user-invocable`; neither path changes broker policy. Existing sessions retain
  their configured active tool names, including deliberate deactivations.

- **The advisor actor is the only writer of `advisor/feed/cursor` and
  `advisor/guard`.** Both are `fact.custom` cells under
  `api.advisor_fact_prefix`, a reserved corner of the namespace: the
  ordinary `put_fact` refuses the prefix and `facts` hides it, so the
  actor writes through `put_reserved_fact` and reads back with the plain
  `fact`. Forging either therefore takes two independent failures rather
  than one, because the fact write a model reaches is the Agency
  blackboard and that composes every key from `agent/` and the calling
  strand's own name. The cursor is the cell that matters: a large integer
  written under it would move the reviewer past everything the primary
  will ever append, and the symptom would be a quiet advisor rather than
  an error anybody sees. The cursor advances only
  after a feed has been committed onto the advisor's branch, or when a
  scan found entries that all rendered to nothing — a stretch of custom
  rows would otherwise be rescanned and re-skipped at every run end
  forever. A feed that fails to send leaves the cursor alone, so the next
  run end offers the same stretch again.
- **Backpressure is coalescing, and the advisor's own run end is exempt.**
  A primary run end whose advisor already has a run open sends nothing and
  leaves the cursor, so the stretch it has not seen arrives later in one
  larger slice rather than in a queue of small ones. `AdvisorRunEnded` does
  not make that check: the driver resolves `run_end` before the run closes,
  so the advisor still reads as busy at exactly the moment its review
  finishes, and a catch-up that yielded to that would never catch up on
  anything. What gates it instead is a debt: a coalesced feed sets
  `Memory.owed`, and a review end with nothing owed sends nothing. Without
  the debt a review end would send any delta past the cursor, and since
  the primary appends throughout its own run it would find something,
  feed it and be asked again whether or not anybody wanted a mid-run
  review. The debt lives in the actor's heap, not in a cell: a restart
  that forgets one delays a review to the primary's next run end.
- **The step threshold is a floor, and the loop above is what makes it
  one.** A feed owed part-way through a run while the advisor is reading
  is coalesced and owed like any other, so the review end pays it and the
  cadence settles at one review per review duration rather than one per
  `feed_every_steps`. That is deliberate — it is what bounds staleness
  when the reviewer is the slower model, which is the documented pairing —
  and it is the feature's real cost. `feed_every_steps = 0` is the way
  back to the run-end-only cadence. The count restarts whenever a slice is
  *offered*, coalesced ones included, so a run end always resets it and a
  short run cannot inherit a long one's count.
- **The strand driver never waits on a review.** The advisor's `run_end`
  and `usage` hooks cast and return the inner answer; nothing about a feed
  — the branch scan, the render, the send, the durable writes — runs on the
  driver. The one bounded wait is `run_start`'s nudge drain, because the
  nudges have to be in the message list that slot returns, and a slow or
  absent actor yields no nudges rather than a stalled run.
- **`advise` is registered for the session and granted to one strand.**
  The tool registry is per session, so the definition exists once; the
  primary's `active_tool_names` withholds it and
  `advisor.ensure_strand` adds it to the advisor's. The seam refuses a
  call by the caller's durable strand name in any case, so a registration
  mistake is a refusal rather than a second operator. A host that
  deactivated `advise` seeds no advisor strand at all: `ensure_strand`
  refuses by name rather than leaving a strand with a driver, a model and
  nothing it can say.
- **A linked git worktree widens the session base to its git directories.**
  `workspace_policy.widening_linked_worktree` reads `<workspace>/.git`; when it is a
  `gitdir:` file, the named directory and the main repository's `.git`
  its `commondir` points at join `writable_roots`, because a jailed
  `git commit` must write the index lock and objects there and both sit
  outside the workspace. It is the trust a primary checkout's `.git`
  already has. A primary checkout, a non-repository, or an unreadable
  `.git` file leaves the base untouched.
- **A jailed child's environment reserves five names per session.**
  `workspace_policy.session_environment` gives every tool shell, satellite and hook
  host the same `PATH`. The bundled toolchain stays first, followed by the
  explicit additions and host PATH, so arbitrary installations are discoverable
  without naming language-manager directories. Lookup does not widen filesystem
  reads. The other server-owned names are
  `HOME` (`<workspace>/.codemode/home`, so what a toolchain writes to its home — macOS makes a
  `Library/Caches` — stays out of the operator's tree) and `TMPDIR`
  (`<workspace>/.codemode/tmp`, a writable fallback). `LOOM_SCRATCH_DIR`
  starts empty so call requirements include the name; the helper replaces
  it with the execution's actual scratch path or removes it when none exists.
  The session base grants both temporary-directory names, because the policy
  meet keeps only names both sides allow. Compiler-owned `TMPDIR` values
  remain unchanged.
- **Git receives identity defaults, not the operator's full configuration.**
  Before runtime admission, `git_identity.prepare` queries configured global
  `user.name` and `user.email` through a read-only broker call in the workspace.
  Git resolves conditional includes; inaccessible or malformed configuration
  produces a warning and empty defaults. The helper's fixed publication mode
  writes `<workspace>/.codemode/home/gitconfig` through directory descriptors
  anchored to the original write grant. It quotes the two accepted keys with
  Git's configuration grammar and creates no unrelated host mountpoints.
  `GIT_CONFIG_GLOBAL` selects that file, which always sets `user.useConfigOnly`.
  Repository and worktree identity overrides retain normal precedence, and
  cherry-picks preserve their original author. No credentials or hooks are
  copied. A write refusal aborts assembly; host code never follows a planted
  tool-home symlink to publish this file. Imported operator hooks keep their
  original HOME and ordinary global Git configuration.
- **Jailed Git calls use the host-resolved binary, never the Xcode shim.**
  `serve` fills `worktree_diff.Wiring.git` once per assembly from
  `host_git.program`, and both `git_identity` and `worktree_diff` name it in
  `argv[0]`. On macOS `/usr/bin/git` is a shim that runs `xcrun`, and a miss
  in `xcrun`'s per-user cache (keyed partly on HOME) rewrites the whole cache
  file. That rewrite runs under the call's one-megabyte `fsize_bytes` limit;
  once the cache exceeds a megabyte it is killed by `SIGXFSZ` (exit 153)
  before Git starts. `host_git` therefore asks `/usr/bin/xcrun --find git`
  on the host, outside every jail, only when the Git on `PATH` is exactly
  `/usr/bin/git` on Darwin, and keeps the found path if `xcrun` fails. Linux
  and a non-shim Git are unchanged. Under `WorkspaceReads` on macOS the
  Seatbelt system view grants `/Library` but not `/Applications`, so an
  Xcode.app toolchain's Git is unreadable there; it was already unreachable
  through the shim, which execs the same binary.
- **The blob store lives in the daemon's state, not the workspace.**
  Content-addressed artifacts (oversized tool output, `report.emit`, job
  spill) used to be `<workspace>/.blobs`, protected from every jail. On
  macOS Seatbelt renders `protected` as unreachable, so once an artifact
  existed a model's `ls -la` in the workspace exited 1 (`ls: .blobs:
  Operation not permitted`) and `git status` warned; Linux masked it with an
  empty tmpfs and left a mount point in the checkout. `serve.session_blob_root`
  now answers `blobs/` beside the session's domain files:
  `<state root>/workspaces/<sha256 of the canonical workspace path>/blobs`
  for a workspace-private domain (one store per workspace, so no other
  workspace resolves its ids), `<state root>/domains/sessions/<id>/blobs`
  for a session-only domain, and `<session dir>/blobs` for a standalone host
  with no domain. The state-root masks `workspaces/` and `domains/` already
  keep every jail out, so `base_policy` carries no `protected` entry and the
  workspace holds no harness directory but `.codemode`. A standalone host
  has no state root, so `protecting_standalone_blobs` protects the store
  unconditionally (`workspace_policy.protecting_blobs`), like the index
  database, since `prepare_directories` creates it before any jail runs. The
  root is an input to the workspace half, not something the plane derives:
  `serve.assemble_in` computes it once, `serve.workspace_spec` puts it in
  `WorkspaceSpec.blob_root` (with the standalone protection already applied to
  the spec's base policy), and `workspace_plane` hands the one string to the
  tool context, the code-mode `into_blobs` and the jobs spill. The census
  carries it back as `Census.blob_root`, which is how an owner whose workspace
  is on an executor learns where that machine keeps the store. `tools/fs` treats `Ctx.blob_root` as a readable root for its
  authorization, because the store is outside the workspace and `fs_read`
  opens refs by path under `read_scope = "workspace"`; writing it is not
  granted. `client/blobs.adopt_legacy` runs in `serve.assemble_in` right after
  `workspace_plane.prepare` has made the store, for a local workspace only: it
  copies each `sha256-<64 hex>` file from a workspace's old `.blobs` into the
  new store after checking that the bytes hash to the name, skips (and logs)
  one that does not, a link, a non-file or one over 256 MiB, stops after 5 s
  and continues at the next start, and never deletes anything. It refuses a
  `.blobs` that is itself a link (`link_info`, not `is_directory`), which a
  jail could point at another workspace's store, and remembers lasting
  refusals in `.legacy-rejected` in the new store so planted garbage is not
  re-hashed at every start. The old
  directory is an ordinary directory afterwards, and nothing reads it:
  `worktree_diff` still excludes it from the untracked listing.
- **A jailed Go tool's caches live outside the checkout, per workspace.**
  `client/gocache` locates `<cache>/loom/workspace/<sha256 of the
  workspace path>` (`<cache>` as `lsp_places` resolves it) and
  `workspace_policy.session_environment` points `GOCACHE`, `GOMODCACHE` and
  `GOLANGCI_LINT_CACHE` beneath it; they used to land under the tool
  `HOME` inside the operator's checkout (20 GB measured). The root joins
  `writable_roots` through `gocache.admitting` in `session_base`, the same
  path a linked worktree's git directories take, and the bash tool asks
  for the base's roots so the grant reaches macOS and Linux alike. The
  build cache is never the host's: `go build` trusts `GOCACHE` entries
  unverified, so a shared one lets a jailed tool plant an object the
  operator's own build links. `[workspace] go_module_mirror` mounts the
  host module cache's `cache/download` read-only, optional so a vanished
  mirror degrades to the public proxy, and sets `GOPROXY` to
  `file://<mirror>/cache/download,https://proxy.golang.org,direct`;
  `[tools]` may not name the owned names (`GOPROXY` only with a mirror),
  checked by `workspace_policy.go_cache_fault` at boot together with the mirror's
  existence and its non-overlap with the workspace, protected paths and
  the root. `[workspace] go_cache_limit_mib` (default 10240) bounds the
  build cache: at session start a weft task measures it with `du -sk`,
  renames it out of the jail-writable root to the sibling
  `<root>.trash-<unique>` when over the limit, creates an empty
  bare replacement with a non-recursive `mkdir` (Go fills it in; a
  recursive create would follow a planted link), and sweeps the parent's
  `<digest>.trash-*` entries (never reading inside the root, since
  `del_dir_r` is path based and the jail could swap a directory for a link
  mid-delete). `locate` returns `None` when the workspace covers the cache
  place, so the root is never inside a jail-writable tree, and
  `gocache.fault` refuses a read-write `[workspace] mounts` entry covering
  the root's parent, which would make the retired trees writable again. A
  rename keeps a live session's open files and fresh opens consistent; a
  build that held the old cache open fails once and a retry clears it. The caches are absent, and
  Go falls back to the tool `HOME`, when the daemon has no cache
  directory. Existing `.codemode/home/...` caches are not deleted.
- **The `[tools]` table selects network and extra environment.**
  `catalog.parse_tools` reads an operator's `network = "off" | "full"`
  (full is the default and what an absent table means) plus `env` names
  read from the host at boot and `[tools.set]` literals;
  `workspace_policy.tool_environment` appends them *after* the five server-owned
  names, and `workspace_policy.under_tools_config` puts the chosen network on the
  session base and every configured name on its `env_allow`. The daemon
  `--network off|full` flag overrides the selected file at session resolution. Both halves
  are load-bearing and neither implies the other: the meet takes the
  narrower network. Bash asks for the base network, while native search
  remains offline and passes only PATH. The meet also
  intersects `env_allow`, which is why a name in the environment but not
  on the allowlist is a narrowing refusal rather than a variable. `PATH`,
  `HOME`, `TMPDIR`, `LOOM_SCRATCH_DIR` and `GIT_CONFIG_GLOBAL` are refused from both lists by
  the parser, so the
  ordering in `tool_environment` is the second lock rather than the only
  one. A configured name the host has not set is skipped with one
  `tools.env_unset` warning, never a boot failure.
- **Envelope decoding is strict; name decoding is tolerant.** `v` must be
  `1`, the discriminator must be present, and a command `id` must be
  present and positive. Unknown `cmd`/`event` *names* survive as
  `UnknownCommand` / `UnknownEvent` so the receiver can answer in band;
  unknown *fields* inside known bodies are ignored (forward compatibility
  within v1).
- **Everything in `protocol` is pure and total.** Malformed input is a
  `ProtocolFault` value, never a crash.
- **A content prompt is admitted atomically.** The gateway decodes every
  block through `core/codec`, validates image base64 and non-empty MIME types,
  and refuses an empty or partly malformed list before `runtime/api` sees it.
  A successful command appends one `UserMessage` in the original block order.
- **Durable JSON crosses verbatim.** Gateway-defined field names are
  `snake_case`, but values that already have a durable form in the
  harness — entries, messages, usage — are carried in `core/codec`'s
  vocabulary (pi field names, camelCase) rather than re-rendered.
- **The wire form is pinned by the golden fixtures** under
  `packages/client/testdata/protocol/`, which this package decodes and
  re-encodes byte for byte. Three details differ from `core/codec` and
  the wire follows the fixtures: `toolCall` blocks nest under a
  `toolCall` key, `thinking` blocks always carry `redacted`, and floats
  print positionally (`0.00027`, not `2.7e-4` — see
  `protocol.to_wire_text`). Changing either side is a protocol change,
  never silent drift.
- **Immutable rows replay exactly; register-backed events replay as
  current state.** `entry` and `usage` events are scanned by seq range,
  so a resume reproduces them; `op_transition`, `escalation`, and
  `strand_result` are read from registers, which keep no history, so a
  superseded phase is not reconstructible. Events are hints and the
  snapshot carries live state, so a client that missed an intermediate
  phase still converges — overlap and gaps are resolved by seq dedup, as
  the protocol already requires.
- **The writer's post-commit publication is production's hint source,
  and the hub joins no hint topic under network delivery** (issue #8's
  aside, decided). A one-session server's writer and hub share a VM, so
  `commit_forwarder` already carries every commit; a bus subscription to
  the hint topics would trigger the same pull twice. `serve` does supply
  a bus since `protocol-change/031`, for the one topic that is not a
  hint: `Outputs`, the rolling tails of running tool calls, published by
  `gateway.tool_output_observer` from the effect wiring and relayed as
  `tool_output` frames. The scope is entered with the idempotent
  `bus.start` because one daemon assembles many sessions. The host
  fixture still joins every topic, which is the shape a host whose hints
  are not all its own writer's — a projection, a second node's session,
  telemetry — takes, with `events/bus.bridge` as the seam that feeds it.
- **Live materialization is a pull, never an apply.** Both the writer's
  post-commit publication and any bus publication merely trigger a pull
  from storage above the hub's high-water seq; a lost hint costs latency,
  never an event.
- **Stream deltas are ephemeral.** They are broadcast without a seq,
  never persisted, and the tap lives entirely in the composition seam —
  the runtime is untouched by it.
- **A network envelope with no `reply_to` leaves through `deliver`, and
  through nothing else** (`protocol-change/018`). `send_to` splits on the
  envelope, not on the connection: `Some(_)` keeps the bounded
  `send_response` path with its one reply capability, `None` goes to
  `deliver`, which re-resolves the binding with `check_binding` for that
  one frame and retires the attachment when the answer changed. Push
  therefore opens no second way out and needs no new authority path; it
  reaches the existing one from the network side.
- **A pushed durable frame is a notice, with one bounded usage exception.**
  The hub emits one `committed` per new emit — the seq and the strand — and
  records still travel the credited snapshot path. A usage row linked to an
  entry claimed by one branch scan also pushes its fixed-shape counters under
  the distinct `usage_observation` name after checking the encoded frame
  against 64 KiB. A row with no entry, a fallback attribution, or an entry
  shared by branches sends only the notice. This observation can drive a live
  cache reading but does not own
  cumulative totals; a missed push is repaired by the capture. Protocol 047
  narrows protocol 018 at this boundary. Delta text is
  clipped with `preview_text` before it is encoded, so a pushed frame is
  under the reply ceiling by construction rather than by a second
  mechanism. A notice is idempotent and order-free: a client that already
  holds the seq drops it, and any later catch-up repairs one that was
  lost.
- **The hub primes its high-water under network delivery too.**
  `start_with_delivery` runs `pull` before serving under either delivery.
  Without it a restarted hub's first hint would push a notice for every
  sequence the store already held. The prime reads the whole history once
  per start, so a supervised restart of a long session's hub pays that
  read again.
- **Human input is bounded hub memory with explicit priority.** Busy prompts
  and follow-ups retain their submitted author and content until admission.
  Steering joins a higher-priority FIFO and stops the observed operation;
  explicit Escape marks the existing `HeldQueue` as `Halted`, and nothing held
  drains while that mark holds. The next client submission on the strand
  (`release_halt`) flips it to `AllHeld`, and one `api.prompt` then admits
  every original message and the release together (`protocol-change/033`).
  Natural completion and steering retain `OnlyHead` draining.
  Removing an empty queue also removes its drain mode; reads and edits preserve
  the mode while retaining each item's identity. Each strand has four normal
  and four steering slots, and a submission that lifts a halt is exempt from
  that bound because it drains the queue rather than growing it. `queued`
  acknowledges transient custody, and a gateway restart can lose unadmitted
  input. `pending_inputs` exposes bounded excerpts keyed by server-minted
  monotonic item identity, while `input_queue_changed` requests a refresh even
  when the durable cursor did not move.
- **Graceful drain is a permanent gateway admission fence.** `Draining`
  refuses mutations on existing sockets while retaining authenticated reads.
  It also disables idle-queue admission before `serve.drain_instance` requests
  runtime abort, so a terminal hint cannot start a held successor. One deadline
  covers the registry snapshot, all resident callbacks, socket flushes and
  runtime settlement. Socket acknowledgements do not prove client receipt;
  expiry and disconnection leave return unconfirmed. [Protocol 038](../../protocol-change/038-held-input-custody-return.md)
  preserves the queue's memory-only contract and documents this limit.
- **Held input edits preserve admission identity.** `QueuedInputGet` returns
  complete text and revision only to the currently mutable original principal.
  `EditQueuedInput` compares the held ID and revision in the gateway actor,
  preserving FIFO position, priority, author, timestamp, and images. Missing or
  drained input and stale revisions return conflict; no edit creates a prompt.
  Per-connection `pending_inputs.editable` is derived from authenticated binding,
  never the display origin. Protocol 024 bounds complete encoded documents to
  48,000 bytes, refusing oversized text instead of editing an excerpt.
- **Worktree observations release the command lane.** `WorktreeDiff` is an
  owner-only read of the attached workspace. `serve` supplies the final policy
  and existing broker to `client/worktree_diff`; Git runs with read-only
  filesystem grants and fixed arguments in the sandbox. The gateway admits at
  most two observations, one per connection. A Weft run retains the original
  socket cancellation witness and a fourteen-second deadline. Pending is the
  ordinary reply; ready and failed are pushes without `reply_to`, correlated
  by `board.request_id`. Delivery revalidates authority. Status paths remain
  literal identities; at most 24 files and 40,960 encoded board bytes are
  retained, with omitted files and partial patches explicit. Patches the
  8-second capture deadline does not reach are omitted the same way; only
  status and the metadata probes fail the observation. The pinned HEAD
  and subsequent filesystem reads form an observation, not an atomic snapshot.
- **Current context is a server observation.** `ContextGet(strand)` captures
  `StrandConfig` and `StrandLeaf` together, then projects the immutable branch
  through its latest compaction. `client/context_view` prices the pinned prompt,
  active unique tool definitions, and projected messages. A usable provider
  total already includes static components; `runtime/hooks.context_tokens`
  adds only newer message estimates. Carried pre-compaction usage is excluded.
  The separate compaction count preserves the existing policy's message-only
  fallback. This ordinary session read shares the worktree observer's managed
  worker slots and deadline, but does not grant worktree authority. It returns
  at most 48,000 encoded board bytes, with complete category totals and explicit
  detail omission. No model, hooks, instruction discovery, or billing scan runs.
- **Live-job observations use the existing lifecycle owner.** `LiveJobs` asks
  the session jobs actor for nonterminal records owned by one strand. The actor
  folds its existing dictionary, retains at most `max_jobs_per_strand` rows,
  and reports total and omitted counts. Starting, running, and draining jobs
  retain their actual `started_by` operation, command excerpt, age, and
  deadline. A missing or unresponsive actor is unavailable, never an empty
  roster. This explicit read adds no historical jobs to ordinary captures.
- **Summaries are presentation and degrade to nothing.** `block_summaries`
  (`BlockSummariesGet`, 1–32 `{entry, block}` pairs, entry ids checked by
  the decoder) is read-only and observer-safe; the gateway reads one exact
  `fact.custom` key per block through `blocksummary.read`, answers a
  `snapshot` in mode `block_summaries` with `{summaries: [{entry, block,
  text}]}` holding only the blocks that have one, treats an undecodable
  cell as absent, and reports a failed reader as it reports any capture.
  After replying it hands the blocks with no summary to
  `Options.summary_demand` (`with_summary_demand`, which `client/serve`
  fills with `blocksummary.ask_for`) as a cast, so the reply never waits on
  the summarizer.
  The `summary/` cells are never in a capture plan: the capture copies the
  whole `client/` prefix, and one cell per long block there would push
  long sessions past the capture's 1024-cell and 1 MiB bounds.
- **The advisor's pending-nudge queue is a read-only observation that
  never drains.** `AdvisorPendingGet` (wire `advisor_pending`, empty
  body, unscoped: a session has one advisor and one primary) answers a
  `snapshot` in mode `advisor_pending` with
  `{strand, observed_at_ms, pending: [string], total}` — the queue oldest
  first, exactly as queued. `client/advisor_pending.read` takes one
  exact-key `fact.custom` selection for `advisor.guard_key` through the
  bounded snapshot reader and decodes it with `advisorguard`'s own
  decoder; it never asks the advisor actor and never calls
  `take_pending`, so a terminal fetch cannot drain what the primary's
  run-start and run-end hooks are entitled to, and a client may call it
  repeatedly. A missing guard cell is an empty board — a session whose
  advisor has nothing waiting — but a store that will not answer, or a
  cell the decoder rejects, is `unavailable`, never an empty success: an
  empty board is the positive claim that nothing is waiting, and an
  unreadable cell is not evidence for it. The board needs no `omitted`
  field, since the guard admits at most `pending_cap` nudges totalling
  `pending_bytes` and the whole queue always fits. See
  [protocol 039](../../protocol-change/039-advisor-pending-observation.md).
- **Provider previews have request and observer custody.** Each pushed delta
  and terminal marker carries the durable request coordinates as `generation`.
  Preview observations retain their source; releasing an old observer cannot
  clear a newer source. Releasing the current observer removes its sample.
- **Notes are an auxiliary bounded read.** `client/notes_view.read` captures
  `agent/<strand>/` independently of conversation metadata. It returns current
  values and revisions, at most 4096 bytes per value and 48000 encoded bytes
  of rows. Oversized boards return `notes_too_large` without poisoning normal
  captures. Tool-availability metadata names actual registry entries and the
  host's code-mode discovery diagnostic separately from strand configuration.
- **Socket admission is the websocket process's first handler turn, never
  its initializer.** mist starts every websocket process with a hard 500 ms
  initializer budget it does not expose, and a missed budget kills the
  process together with its TCP socket, which the peer reads as an abrupt
  close rather than a refusal. So `client/server`, `client/daemon/server`
  and `client/daemon/session_socket` all have `on_init` mint their
  subjects, send themselves `Admit`, and return; the root's permit
  transfer and the gateway attach — six seconds of budget between them —
  run on that message. The self-send is ordered ahead of every frame
  because mist hands over the socket and calls `set_active` only after the
  initializer has returned, so `Admit` is already queued when the parser
  is armed.
- **A deferred transfer owes its HTTP process a barrier.** Both daemon
  sites release the reservation and then exit the moment the upgrade
  returns, and `root.transfer` refuses a reservation whose HTTP owner has
  released it or died. The websocket process therefore signals a subject
  owned by the HTTP process as soon as the transfer has been attempted,
  and the HTTP process waits for that signal before releasing. It is a
  consumed reply from the one process that sends it, not an assumption
  about two senders, and it adds no waiting the initializer did not
  already impose — only the deadline moved.
- **Every upgrade without the exact bearer token is answered `401`**
  before any websocket state exists. `LocalAuth` binds loopback and puts
  the token in a `0600` file next to the session, moving the
  peer-credential check into the filesystem, because `mist` has no
  unix-socket listener to carry `SO_PEERCRED`.
- **A strand's durable `active_tool_names` is canonical: sorted, with
  duplicates collapsed, and every name registered.** The list renders
  into the request's tool array, which sits ahead of the system prompt
  in the provider's render order, and prompt caching matches on an
  exact byte prefix — so a reordering costs both cache-head writes on
  every subsequent turn. The hub canonicalizes at the `set_config`
  boundary and `client/wiring.tool_specs` sorts again at render.
  Neither moves the authorization line: `wiring.clear` admits a call by
  `list.contains` on this same list, and set membership is blind to
  order and multiplicity.
- **The system prompt is assembled at a session's first open and pinned
  with its enforcement demand.** Every later boot at the same demand sends
  the pinned bytes rather than deriving them again — the agent may have
  edited `CLAUDE.md`, the kernel may have changed under a restart, and a
  prompt re-derived from moved inputs is a full one-hour cache write on the
  first turn after every restart. The demand is the one deliberate cache
  break: a changed flag or a legacy pin with no demand identity renders and
  pins once, because keeping bytes that claim stronger enforcement than the
  broker now demands would trade truth for cache stability. Both cells live
  under the reserved `prompt/` prefix, so no model-reachable `put_fact` can
  rewrite the operator's channel.
- **The pin is read before `api.open` and written after it.**
  `wiring.Config.system` must hold the string before the open, and the
  open is what stands the writer up — so the cells are read straight off
  the session store while nothing owns it, and written back through
  `api.put_reserved_fact` once there is a writer to journal it. Same knot
  as the Agency's, solved by ordering rather than by a name because the
  value is data, not a process.
- **Nothing volatile may enter `system_prompt.Host`.** No clock, date,
  elapsed time, token count, cost, git state, id, strand name or random
  value — and no numeric field at all, which is the shape all of those
  arrive in. The list fields are sorted and de-duplicated by
  `pack.environment` before they can reach the bytes, for the same reason
  the tool array is sorted: both sit inside the cached prefix. The one
  exception is `available_tools`, the prompt's tool index, which is
  de-duplicated but not sorted: its order is the registry's registration
  order, which is fixed for the session and is what a reader wants.
- **A prompt pack refuses loudly or serves; it never disappears.** A pack
  file that cannot be read, does not decode, or renders to nothing stops
  the boot with a worded message naming the file and the fault — a
  silently-empty system prompt is the failure this seam exists to end. A
  pack that merely trips `pack.problems` (a dropped section, a misspelled
  placeholder) warns on stderr and serves: `decode` accepts more than
  `problems` approves by design, and a thin prompt beats a dead server.
- **The operator's instructions layer under the workspace's, and only a
  file that is not there lets the global lookup move on.** Slot one is
  the operator's global `AGENTS.md` — `~/.agents/AGENTS.md`, then
  `~/.loom/AGENTS.md`, first found wins — carried into every session;
  slot two is the workspace's `AGENTS.md` and slot three its `CLAUDE.md`,
  neither of which has a fallback. A workspace `AGENTS.md` that exists
  but is oversize or unreadable warns and leaves its slot empty rather than
  falling
  through to the operator's defaults: serving standing operator
  instructions in place of a project file that happened to be unreadable
  would swap one set of instructions for another behind everyone's back.
  Every one of these reads warns and continues; none of them can stop a
  boot. `HOME` unset is the launcher's hard failure degraded to a
  warning, because a missing instruction file must never cost a session.
- **The fence around an instruction file is the harness's, and its
  `origin` attribute is a claim only the harness can make truthfully.**
  At most one `user-default` block exists and it is always the first,
  which is what the pack's prose tells the model to check; the bytes
  inside a block are otherwise verbatim, and framing a hostile file does
  not make it safe — it stops it speaking with the operator's voice, and
  the residual risk is accepted and named, exactly as it was for the
  single-file case.
- **The Agency is reached through a name, never through a captured
  runtime.** `api.open` takes the `Effects` record and returns the
  `Runtime`, and `Runtime` *contains* `effects` — so a closure reachable
  from `Effects` that captures the `Runtime` is a value cycle, not an
  ordering problem. `agency.seam` closes over a minted process name (the
  same indirection `gateway.commit_forwarder` uses) and `agency.start`
  stands a holder up under it after the open. The holder answers exactly
  one message and answers it with the runtime *value*: the tools do the
  work on their own effect process, because a holder that did the work
  would serialize every agent call in the session behind whichever one
  was inside a wait.
- **A strand may wait only on a descendant and address only its parent or
  a descendant** — decided from the durable lineage ledger, never from
  anything a model says. A strand with no lineage cell is a root and is
  nobody's descendant: "no lineage fact" fails closed. That is what makes
  the wait graph acyclic and a blocking `agent_wait` safe.
- **The bound with an answer fires first.** Two capabilities this package
  services answer on bounds of their own, and both must sit *below*
  `client/codemode.default_call_timeout_ms` (120 s): `client/mcp`'s
  `default_call_timeout_ms` (60 s) and `agency.default_config`'s
  `max_wait_ms` (30 s), the ceiling one `agent_wait`/`strand.wait` is
  clamped to. A `ServedHere` call the satellite host gives up on is
  answered `unsettled` and its worker killed, so inverting either ordering
  would trade a real answer for that refusal on *every* call. The
  constants live in three modules and nothing but a test can hold them in
  relation, so each is pinned by one:
  `the_mcp_call_timeout_wins_the_race_test` and
  `the_wait_ceiling_wins_the_race_test`.
- **Natural completion waits for attached children.** `agency.reaping_hooks`
  reads current child operations through the separate writer and session store,
  without calling the parent driver.
  A running parent with active attached children receives a born-placed
  follow-up naming their current waitable handles, including resumed runs.
  It retains the same operation until those children settle. Detached children
  do not delay it. Cancelled parents retain cleanup, and overdue children are
  observed through the existing deadline enforcement. Ledger read failures
  return a follow-up instead of permitting completion.
- **Terminal parent cleanup runs in the background.**
  The completion guard reads through the separate writer and observes overdue
  child budgets by recording stop intent and sending cancellation, without
  waiting on either driver. Final parent cleanup runs on a background process.
  Each current child-run record names its owning operation, including custody
  transferred on continuation.
- **A reap's intent is durable and its abort is re-issued.** `api.abort`
  is a no-op when no driver is registered, so a reap that landed
  mid-restart would otherwise evaporate and the child would run until the
  session closed. The mark is written once and every later observation
  re-issues the abort.
- **A spawn adopts an existing child on a name match only when the ledger
  says this caller minted it.** Handing an existing child back is the
  reconciliation path that makes `agent_spawn` `ReplaySafe`, and it is
  worth keeping — but a name is *derived* and a lineage cell is
  *recorded*, and only the second is evidence. So `minted_by` is compared
  against the caller's own call site — the operation, `agent.minting_step`
  (the step with the minter folded in, since `lineage.CallSite` has no
  field for a program's ordinal) and the source index — and a mismatch is
  refused as `agent.NameAlreadyMinted`, never adopted. Adoption without
  that check is an ownership transfer rather than a reconciliation: the
  adopting spawn's brief, tools, `within_ms`, `detach` and `result_schema`
  are all discarded (`write_result_schema` runs only on the minting
  branch), `check_capacity` is skipped so the adopted child costs nothing
  against `fan_out`, and the caller goes on to wait on a strand doing
  somebody else's work and report its answer as the answer to a question
  it never asked. The name derivation is what makes a collision
  unreachable; this is what makes one harmless if it were reached.
- **The blackboard is clamped on both sides.** `agent_note` writes under
  `agent/{caller}/`; `agent_notes` with no prefix reads `agent/` and not
  the whole non-reserved fact namespace, which is what
  `api.facts(prefix: None)` would hand back.
- **The todo cell has one writer.** `agent/{strand}/todo` holds the
  strand's `core/todo_list` board, and `note` refuses the `todo` key, which
  covers `cap/notes.put` too since it reaches the same closure. The
  `todos` closure is the only writer: it reads the cell with
  `api.fact_cell`, runs the tool's pure step, skips the write when the
  board is unchanged, and commits with `api.put_fact_expecting` against the
  sequence it read, retrying up to `todo_attempts` times on
  `FactConflict`. A conflict can come only from a sibling `todo` call in
  the same batch, so the retry is what lets the tool stay `Concurrent`.
- **A result contract is enforced on the child's write, not on the
  parent's read.** A spawn's `result_schema` is stored at
  `result-schema/{child}` — written *before* the lineage cell, so a crash
  between the two can leave a schema with no child and never a child
  whose contract vanished — and quoted into the child's brief. The check
  then runs inside `note`, on the child's own `agent_note` call, because
  the child is the party that can repair the value and the moment it can
  repair it cheaply is the run that produced it; checking only at the
  join would tell the one party that cannot act. `wait` validates again
  on the way out, not as a second authorization but because the cell
  comes back out of the durable store and a value crossing that boundary
  is decoded rather than trusted. The contract cell sits outside
  `agent/`, which is the whole of what keeps it out of a model's reach:
  `agent_note` prepends `agent/{caller}/` and `agent_notes` lists under
  `agent/` alone, so a child can neither rewrite what it is judged
  against nor read what its siblings were asked for.
- **A report into a *finished* parent is refused.** `api.send_to_strand`
  accepts a fresh run when the target is idle, which would wake a
  finished parent with no human present — the exact property auto-
  enqueued child results were rejected over. The refusal is upward only:
  a parent giving an idle child more work is a live agent's explicit
  decision inside its own run.
- **The rule scanner's "provably dead" is the ledger's `reaped` mark,
  never a terminal `strand.last_result` alone** (issue #113). The
  asymmetry above is exactly why: since a live ancestor may still hand
  an idle *child* fresh work — never refused, unlike the upward case —
  a finished strand is not a dead one, and `client/rulescan` would
  silence an operator's rule on a strand that might fire again tomorrow
  if it treated "finished" as "will never run again". Only
  `client/agency`'s own decision to end a child (an overdue deadline, or
  its parent's run ending undetached — `reap`, in `client/agency`) is
  durable and deliberate enough to trust, and even that is a named
  residual rather than a mechanical guarantee: nothing stops a live
  ancestor that still remembers the child's name from addressing it
  after the reap. `client/rulescan`'s module doc has the full argument
  and the FAIL OPEN branches (no cell, a cell that will not decode).
- **A schedule is owned by the strand that created it and lives no
  longer than the strand it fires onto.** Those are two rules and both
  are needed. Ownership is what makes a schedule retirable: cancellation
  and listing are keyed on the creator, so a parent can cancel a
  heartbeat it set onto a child that has already settled — which nobody
  could do while a schedule was cancellable only by its target (#163).
  The target's lifetime is what makes it bounded: `client/scheduleseam`'s
  `run_end` reap retires every schedule keyed to a strand whose brief
  just ended, and `client/schedulescan` refuses to fire onto a target
  the lineage ledger says is reaped or whose brief has settled, so
  neither a lost reap nor an operator's `[[schedule]]` can keep a
  finished child's clock running. A target may only ever be the caller
  or a strand it spawned, decided from the ledger through
  `agency.owns` — the addressing rule above, narrowed — and no schedule
  onto a subagent may wake it, whatever the operator's policy permits,
  because a subagent has one run and a fresh one after its work ended
  would extend a child's life outside its parent's spawn budget. What
  ownership does not move is a schedule's identity: the config, seen and
  fired keys stay `{target, name}`, because an occurrence is a fact
  about the target's timeline.
- **A code-mode submission is judged against the seam it named, and
  routed by the seam the host wired.** The two halves are read from
  different places on purpose. The allowlist follows the *submission*
  (`exec_config` takes its vetting policy from `Request.seam`), so a
  refusal the model reads is about the surface it asked for. The router
  and the admission ceilings follow the *surface*, so a host serving one
  seam hands out that seam's router whatever a request names and no
  submission can widen what the operator wired. A request naming a seam
  the surface does not serve is refused by the tool shell before
  `execute` is called and again by `execute` itself — `StartFailed`,
  nothing dispatched, both stages unreported — rather than being
  reinterpreted as the seam the host does serve. The residual mismatch
  that arrangement allows can only narrow: a program vetted against one
  seam's imports and routed by the other's cannot import the modules
  whose calls that router services, so it reaches nothing at all.
- **A code-mode policy refusal is raised once, for the whole execution,
  and only from the satellite launch.** Code mode clears through the
  broker the pipeline holds rather than through `Ctx.clear_call`, so
  until #97 a refused execution reached no escalation plane and nothing
  could mint the grants #24 taught it to spend. `execute` now wraps the
  pipeline's launcher, re-asks the launch's own composition question
  (`launch.node_requirements` ⊕ the base ⊕ the identity's grants, so
  there is nothing to drift; the same `Config.host_mounts` reaches both
  sides), and reports the shortfall outward as
  `tools/codemode.RunRefused`; `wiring.tool_context` supplies the raise
  seam that turns it into a durable record, and the tool shell
  re-executes once on an approval. Three clearance points, one raise: the
  hermetic build composes with the grants already dropped, so no approval
  can widen it and the question would be unanswerable; a capability call
  refused inside a *running* program is refused after effects have
  happened, and the one thing an approval buys is a re-execution, which
  `replay: tool.Never` says must never happen. The launch is the only one
  where nothing has run yet, so the re-execution repeats no effect and
  the action a human consents to is still the whole submitted program.
- **The action a code-mode approval binds to is the program.**
  `wiring`'s raise builds `escalate.Refused` with `tool: "code_mode"` and
  `arguments: run.arguments` — the same post-clearance arguments the
  broker path uses — so `record_id` digests `{strand, "code_mode", wanted
  diff}` and `action_digest` digests the submission. A retry of the same
  program inherits the approval and spends it; a *different* program
  wanting the same diff lands on the same record id, fails the action
  binding, and re-opens the question with no grants (#65). That is the
  seam where consent would most easily leak, because the want is coarse
  and the program is everything.
- **A code-mode execution runs under the calling strand's own
  `{op_id, step_id}`.** The hermetic build, the jailed `erl`, and every
  capability call the running program makes are dispatched under that one
  pair, which is the *batch* identity the broker pools budget on —
  so the compile and the run share one ledger and one wall deadline
  rather than minting a second budget, and `broker.abort` on the
  operation reaches the build and the node alike. The pooled
  outstanding-effect cap is never below two: the node holds one for its
  whole life.
- **Both jailed stages' enforcement reports travel in the outcome.**
  `pipeline.execute` returns them beside the outcome — the build's from
  `compile.Compiled`, the node's from `satellite.Run`, whose report is
  what `CapConnection.destroy` returns — so this module does not wait on
  a mailbox and has no grace period to lose. It only restates them as
  `tools/codemode.Report`, splitting the helper's `skip:` entries out of
  the applied list so no renderer can present a skipped layer as an
  enforced one (issue #5).
- **The harness-side capability bridge builds no clearance, and
  re-derives no authorization.** `fs.read`, `fs.list`, `kv.*` and
  `report.emit` are answered as `satellite.ServedHere` — no
  `broker.CallSpec`, no jail, no composed policy — because none of them
  leaves the VM and a policy whose enforcer is not present is not a
  check (issue #16). What contains them instead is `tools/fs`'s own
  `resolve_real`, called by the injected closure: the *same* function
  `fs_read` calls, so a symlink planted inside the workspace and pointing
  out of it is refused for the bridge exactly as it is for the model's
  own tool. Writing a second resolution here would be a second boundary
  to keep correct, and the one that got it wrong would be the one nobody
  was reading. The write arms are wired through the same discipline:
  `fs.write` resolves through `resolve_writable`, so a satellite program
  meets the protected-path refusal (#105) exactly where the model's own
  `fs_write` does, and `fs.edit` is read-apply-write inside one served
  call under `codemode/workspace.apply_replacements`' exactly-once
  find/replace ruling — the tightest window the seam can offer, and a
  strictly tighter one than the two-round-trip composition a program
  would otherwise hand-roll.
  The write itself is `fs.write_whole`, which is what `fs_write` calls,
  so **both doors create missing parents** and `new_dir/file.txt` means
  one thing whichever door it came through. A `fs.list` of a
  non-directory is `NotADirectory` under the `not_a_directory` code
  rather than an errno sentence, because it is the one listing failure a
  program can act on — `fs.read` was the call it wanted — and
  `readdir`'s own `ENOTDIR` brings the special files (FIFO, socket,
  device node) under the same honest name.
- **A base policy the sandbox cannot enforce is a boot failure.**
  `Settings.base_policy` is a field, so a host may hand `boot` any
  policy value it can construct, and `assemble` asks
  `base_policy_fault` before a directory is made, a lease is taken or a
  helper is spawned. The case that motivated it: a relative `protected`
  entry is refused by the jail as `RelativePath` and covers nothing in
  the harness's own path checks, so an operator who wrote
  `protected: [".git"]` would otherwise get a session protecting nothing
  and learn about it from the first tool call of it. The check is
  `broker/policy.validate`'s — the same one every composed policy passes
  immediately before dispatch — so what refuses here is exactly what
  would refuse there. Pure and separate from `boot`, because it is a
  decision about a value and should be testable as one.
- **A blob is established by a rename, never by a write.** `report.emit`
  writes through `blob.write_addressed`: the bytes are staged under a
  temporary name in the blob root and renamed into place, so a crash
  mid-write can never leave a partial file at an address whose SHA-256
  name vouches for the whole of it. The staging name carries eight
  random bytes off `Config.entropy`, because two concurrent first
  emissions of identical bytes are precisely the pair a content address
  cannot separate and a shared staging name is where they interleave.
  Idempotency is untouched: the destination is the address either way,
  and the `is_file` probe still skips the work.
- **`report.emit` is one mechanism on two seams, and the one workspace
  capability with a ceiling.** `cap/report` is the only module both
  vetting allowlists carry, so both routers answer `emit` — from the same
  closure, into the same blob root, under the same content address and
  the same 64-per-execution ceiling (`codemode/artifact`). It earns a
  ceiling where `fs.*` and `kv.*` do not because every admitted call
  writes something that *outlives the execution*, which is
  `satellite.CapCeiling`'s test; the scratch store is bounded store-side
  by bytes with eviction instead, which is the right instrument for
  something that must not grow rather than something that must not be
  called often. So the workspace seam's ceiling list is no longer empty,
  and the comment saying it was is gone.
- **The only thing the code-mode wiring adds to a session base is two
  environment *names*.** `LOOM_CAP_SOCK` and `LOOM_CAP_TOKEN_FILE` are
  set by the launcher and cannot be shadowed by a caller's `env`, but
  policy composition takes the meet — so a base that does not name them
  composes them away and the satellite cannot find the channel it exists
  to speak on. `execution_policy` adds exactly those two and leaves every
  other dimension untouched; it is one small named function so it stays
  auditable rather than diffusing into the wiring.
- **An execution's files live in their own directory inside the
  workspace,** named for `{op_id, step_id, source_index}` and removed
  when the execution settles. Inside the workspace, so the session base already
  makes it writable and nothing has to be widened to build there; unique
  per execution, so neither two strands running code mode at once nor two
  `code_mode` calls in one batch can share a build root. The third field
  is what makes the second true: `code_mode` is `tool.Exclusive`, which
  forbids a concurrent *start* and nothing more, so one batch may hold two
  `code_mode` calls that run back to back under one operation and one
  step. Keyed on the pair they would build in one directory, bind one cap
  socket and write one token file — and the launcher's janitor runs
  teardown asynchronously after the host dies, on ordinary exits too, so
  the first execution's cleanup could unlink the second's live socket and
  token, while `prepare_root`'s recursive delete races the same janitor
  the other way (issue #87). The name is a short digest of that triple
  rather than the triple itself, so a step id cannot put a separator or a
  dot segment into a path.
- **The cap socket does not live in the execution directory** (issue
  #611). An AF_UNIX path is limited to 104 bytes on macOS and 108 on
  Linux, and a socket inside the workspace failed in any workspace deeper
  than about 60 bytes. `codemode.socket_directory` names one directory per
  execution under `Config.socket_root`: `<state root>/run` for a
  daemon-managed session (`Settings.codemode_sockets`, set by
  `resolve_managed`; `codemode.runtime_directory` is the name), or
  `work_root` when a host names none. The key is the same `{op_id,
  step_id, source_index}` triple plus a length-prefixed work root, since
  one socket root serves every workspace, so #87's janitor and
  `prepare_root` races stay closed. `execute` removes it with the
  execution directory. Extension hosts use `host_socket_directory`, keyed
  on the extension name like `host_root`, and keep it for the session.
  `daemon/root.directories` creates `<state root>/run` mode 0700 at
  startup, and `workspace_policy.established_masks` masks it from **every** jail.
  Only the satellite's own base reaches its directory:
  `codemode.reaching_socket` adds that directory as a readable root and
  drops a protected entry only when it lies inside the socket root and
  covers the directory. `protected` has no carve-out verb, so this is the
  only way to reopen a path under a mask; a mask above the socket root
  (an operator masking the whole state root) is kept and the launch
  refuses in band. The build's base and an extension invocation's base
  do not get the grant. Under host reads the satellite (and its
  `proc.run` children, which share its base) can list the other socket
  directories beside its own; they hold sockets only, and each accepts a
  connection only with its execution's token, which stays in that
  execution's workspace directory. The mask stops listing on both
  platforms and connecting only under bubblewrap; Seatbelt allows
  unix-socket connects by path, so on macOS the unlisted digest name is
  the barrier. A session whose writable roots cover `<state root>/run`
  (one opened on the home directory) binds under its own work root
  instead (`serve.codemode_socket_root`). The in-band length refusal remains
  for the residual case and names the socket root, not the workspace.
- **MCP reaches a model through code mode, and a host with no code mode
  starts no MCP server.** A server's tools are a *module* a program may
  import, never a registered tool, so a host that registers no
  `code_mode` has nothing that could ever call one — and spawning
  third-party server processes for it would be cost and attack surface
  bought for no capability. One `mcp.unavailable` line says so, naming
  the servers that were skipped. Per server, every failing step —
  a missing executable, a refused handshake, an unset `api_key_env`, a
  listing the generator will not accept — is one more `mcp.unavailable`
  line and no boot failure, and it is the only thing anybody will ever
  see about it: a refused server has no module, so a program importing it
  is refused by vetting with no word about why the module is absent. A
  working layer logs `mcp.ready` with each server's name and tool count.
- **Every credential name resolves through exactly one store.**
  `Settings.secrets` is the layered `provider/secret.SecretStore` built
  in `serve.resolve`, once per session assembly rather than once per
  daemon: the `[secrets]` table's resolved pairs over the process
  environment. The provider gateway, an MCP server's
  `api_key_env`, an extension's bound egress secret, and the `reading:`
  seam `tool_environment` builds a jailed shell's environment from are
  all handed that same store, so a backend added to `client/secrets`
  covers all four or none. A resolved value never enters the catalogue, a
  session, a transcript, a log line or a `Failure`; `[secrets]` does not
  change what reaches a jail either, because `[tools] env` still passes a
  *value the operator listed by name* and `env_allow` is still a list of
  names.
- **An `api_key_env` is a variable name, and the value is read at spawn
  and never held.** It is resolved through the same `provider/secret`
  seam every other configured secret goes through, put into the child's environment under the same name, and
  never stored in a record, a log line, or a refusal message. A
  configured variable that is unset refuses that server *before* anything
  is spawned.
- **Registration is gated on the seam, for both families.** A host with
  no messaging plane ships no `agent_*` tools and a host with no
  toolchain or no prepared build seed ships no `code_mode` — the boot
  says which is missing, once, as a `codemode.unavailable` log line, and
  the registry it actually built as `server.tools`. The wire tool array
  is the byte prefix of the provider's cached region, so a
  permanently-refusing definition would be paid for on every request of
  every strand for the life of the session. **The message is part of the
  mechanism, not decoration**: it is the only thing anybody will ever
  see about an absent code mode, because there is no tool left to fail
  later, so it names what is missing, both places that were looked, and
  the way to supply it. A working host logs `codemode.ready` with the
  `gleam`, the `erl` and the seed it settled on.
- **A component of Loom is looked for in Loom's own tree before `PATH`.**
  The helper, the emulator and the compiler are all located from
  `code:root_dir()` first (`client/install`). For `loom-exec` that is a
  security property rather than a convenience: it is the binary that
  builds the namespaces and applies Landlock and seccomp, so a stray one
  an unrelated install left on `PATH` must not outrank the audited one
  shipped in the same tree as the running server. An explicit `--helper`
  still outranks both, because that is how an operator points at a helper
  they audited themselves, and a flag naming a missing file refuses
  saying so rather than falling through to one they did not choose.
- **Approved grants are validated structurally against the wanted diff**
  before being stored back in the runtime's internal vocabulary, so the
  consume path hands a re-execution exactly what was approved.
- **A grant reaches a policy decision through the call it was approved
  for, and by no other route.** `wiring.tool_context` builds `Ctx.grants`
  from `effects.ToolRun.grants` — what *this call's* clearance consumed —
  decoded from the escalation vocabulary here, because the runtime moves
  those payloads opaquely. A payload that will not decode is dropped
  rather than faulted on: skipping a grant can only narrow what the call
  receives, and the call still settles in band under whatever remains.
- **A base that covers every shipped tool can never park.** Composition
  refuses only when a tool's requirements exceed the base, and
  `base_policy` grants read of `/`, writes to the workspace, and the four
  environment names `bash` passes — which is every requirement any
  shipped tool has, since all of them are static rather than derived from
  call arguments. So the default posture is one where the escalation
  plane is real but dormant, and a host that wants approvals to mean
  something serves a narrower `Settings.base_policy`. This is why the
  base is a setting: a test cannot otherwise boot a real server into a
  state where a refusal happens at all.
- **Every policy refusal raises a durable record; only some park.** The
  two questions are separate on purpose. Raising is unconditional and
  deduplicated by a digest of `{strand, tool, wanted diff}`, so a model
  that retries lands on the record already pending instead of one row per
  attempt, and the in-band error stands alongside it. *Parking* — holding
  the call open until a human decides — happens only when
  `Config.interactive` says someone is attached, because a parked call in
  a headless session is a hang. Which raised records actually interrupt a
  person stays a client-surface decision: the hub already emits
  escalation events and lists pending ones in its snapshot, so approval
  fatigue is tunable in a UI rather than frozen in a deploy.
- **An approval is consent about an *action*, not about a want.** The
  record id digests `{strand, tool, wanted diff}` and says nothing about
  the command behind it, so two calls that differ only in what they run
  land on one row — which is right for deduplication and catastrophic
  for consent: a yes given to `bash "true"` would otherwise be spendable
  by `bash "curl -T ~/.ssh/id_rsa …"` in a different operation, later in
  the session (#65). So a raise carries `action_digest` of the call's
  effective arguments alongside the id, the record stores it, and only a
  claimant that digests the same inherits the approval. Anything else
  re-opens the record as a fresh `Pending` question bound to the new
  action, and the model sees the byte-identical in-band refusal a first
  denial produces — it must never be able to observe that an approval
  existed and was set aside. Nothing is excluded from the digest: a
  field the consent layer overlooks is a field the model may vary after
  consent. `escalate.spend` re-checks the binding beside the scope check
  and, on failure, settles in band leaving the record untouched.
- **Re-opening is bounded, because the party provoking it is the party
  being constrained.** The mismatch edge above is a *new* way into
  `Pending`, so `Config.max_asks` (3) caps the questions one row may put
  and the claim comes back `Exhausted` past it: nothing written, nobody
  asked, the call settled in band (#66).
- **A claim's checks and its commit are the same read.** The park's probe
  reads an `api.EscalationCell` and spends through
  `api.consume_escalation_at`, which CASes at that seq, so a claim
  landing between the scope-and-action checks and the consume loses the
  commit instead of passing unseen (#68). The cell travels through the
  `weft/poll` loop as the probe's own value, so moving the loop onto the
  primitive did not put a re-read between the decision and the CAS.
- **Both foreground waits make a first attempt immediately and a last one
  at the deadline.** That is `weft/poll`'s contract, not this package's,
  and it changed one behaviour worth knowing: `park` used to check its
  deadline *before* reading the record, so a window already shut settled
  without a read. It now costs one read, and an approval landing exactly
  on the deadline is honoured rather than missed. `agency.wait_loop` folds
  the map of already-settled handles from one attempt to the next, so a
  slice never re-asks the store about a child that answered on the first
  pass, and expiry hands that map back (`poll.RanOut`) so the settled
  children are still reported `Ready`.
- **And so is an approval's.** `approve` carries the diff and the action
  digest the client *rendered*; `gateway.approve` reads one
  `api.EscalationCell`, checks the echo against that value, and commits
  the `Approved` write CAS-guarded at that same seq — which is why it
  builds the transaction itself rather than calling
  `api.approve_escalation`, whose own read the checks would not be about.
  A refreshing claim is exactly what makes the check mean something: it
  changes what the record wants and bumps the register seq, so it either
  fails the echo or loses the commit. A mismatch answers
  `stale_approval` with the fresh record in `details` and writes nothing
  (`protocol-change/007`, #72). The record's `CallScope` is also what
  `op`/`strand` on the wire now come from, in place of a guess at which
  strand was busy (#67).
- **A raise *claims* the record; the scope is the call standing at the
  door now.** The digest deliberately excludes the call id, so under a
  retry the row is already there when a refusal arrives — and the call
  that opened it has already settled, because a model that reads an
  in-band refusal retries under a call id the provider mints fresh. So
  `api.claim_escalation` moves the scope to the live call under a CAS
  instead of refusing the duplicate: a pending record moves and refreshes
  its stored denial, an approved one moves and keeps its grants, and a
  consumed or rejected one re-opens as a new question with no grants.
  Without that the ordinary first-run sequence — refusal with nobody
  attached, a human attaching and approving, the model retrying — leaves
  an approved record scoped to a call that will never come back, which
  nothing can ever spend and nothing can ever replace (the register is
  write-once and there is no delete path). Two consequences are
  deliberate: two calls wanting the same thing at once share one prompt
  and therefore one authorization, and approving a want does not make it
  unaskable — the next call wanting it asks again, because a standing
  permission is a wider base policy rather than an approval that never
  expires.
- **The escalation set is bounded on both axes, because two constantly
  running paths scan the whole `escalation/` prefix** — a tool clearance
  looking for approvals attributed to it, and the gateway's pull turning
  new records into events. *Width*: a limit grant contributes its
  **field** and not its magnitude to the dedup digest, because `bash`
  turns a model-supplied `timeout_ms` into the `wall_s` it asks for, and
  under a narrow base that number reaches the wanted diff — a retry loop
  stepping it would otherwise mint a record, and a prompt, per attempt.
  The magnitude still reaches the human: it is in the record's stored
  denial, which is what an approval is granted against. *Count*:
  `Config.max_records` caps the distinct wants a session will file, and
  past the cap a refusal that would open a **new** record settles in
  band; one that lands on a record already there costs no row and is
  never turned away.
- **A park waits on the tool's own effect process, never on the driver.**
  `Hooks.run_end` is not the only thing that must not block `drive_loop`:
  a driver stuck inside a clearance stops serving `Nudge`,
  `RequestAbort` and `PollTick`, so a human who changed their mind could
  not abort the run they are being asked about. Parking in the broker
  seam — inside `Ctx.clear_call`, on the process the driver spawned and
  monitors — also means the wait needs no timer and leaks no process: the
  effect process is linked to the driver's reaper, so an abort or a
  driver restart kills the parked call and the driver settles it in band
  through the ordinary monitor path.
- **Provider wrappers inherit stream ownership.** `gateway.tap_provider` and
  `wiring.recording_summaries` each return a prepared provider surface. Each
  wrapper publishes a minimal custodian while its guard is parked; only after
  the parent adopts that owner does the guard prepare and adopt the inner
  stream, then grant its begin permit. The guard
  remains the inner stream's direct consumer; only each synchronous observer
  call moves to a monitored worker. The custodian adopts both and the inner
  owner, while the guard retains its own pre-begin drain monitor. It forwards
  cancellation inward and bounds missing acknowledgement with
  `CancellationUnconfirmed`, then stays alive until the registered subtree
  exits. An abnormal transitive Down becomes terminal `DrainProofLost`, never
  retry permission. This keeps the chain continuous from driver reaper to runtime
  custodian, relay custodian and guard, gateway custodian, guard and pump, and
  the native HTTP owner plus its dedicated handler; no wrapper may turn
  consumer death into a detached request.
- **Named helper traffic binds observation and delivery to one incarnation.**
  Ephemeral commit hints, provider deltas, hub casts, and holder calls resolve
  a registered name once, then send its tagged envelope directly to that PID.
  If an optional hub unregisters before resolution its hint is dropped; if it
  exits afterward the PID send is a no-op or the caller's existing monitor
  reports unavailability. Re-resolving the name inside `process.send` would
  leave a race that converts an ordinary restart into a caller crash, or asks a
  replacement while still monitoring its predecessor.
- **Wrapper comments narrate ownership handoffs.** The module story names why
  the guard, observer, and custodian are separate; comments at publication and
  adoption say what becomes safe after each acknowledgement. Do not reduce
  this to comments which merely restate a `spawn`, `send`, or monitor call.
- **A park is bounded by the configured window *and* by the call's own
  budget deadline, and the deadline is re-read immediately before the
  consuming commit.** The second bound is not politeness: the broker's
  ledger refuses a reservation past `deadline_ms`, so a re-clearance
  after that instant is a `BudgetRefused`, not a resumption, and holding
  a call past it would trade an honest "policy refused" for a confusing
  "deadline passed". The re-read is the same fact one step further in:
  the consume is a writer round trip, so a slice admitted just inside the
  window could otherwise commit `Consumed` on its way to a budget
  refusal, spending an approval on an execution that never happens. Both
  bounds are read from the session's own clock, which is why the seam
  must share it (spec §0.2). What the budget bounds is *admission*, not
  total elapsed time: a resumed call re-enters `reserve_budget` with its
  `CallSpec` unchanged and gets the original `wall_s`, so a parked call's
  lifetime is `park + wall`.
- **An approval buys exactly one re-clearance, of exactly one call.** The
  record's `CallScope` is checked — exact equality — before anything is
  spent, so a call that lost the claim to another call cannot burn the
  approval that other call is standing at; the consume is a CAS that must
  win *before* the grants compose into a policy; and the resumed call is
  cleared once, not retried in a loop — if the widened policy still does
  not satisfy the tool, the second refusal stands in band. Only a call
  whose denial digests to the record's own id can ever hold the claim, so
  "one call" is always a call wanting the same thing on the same strand
  through the same tool, and the grants it spends are the ones a human
  chose rather than its own. A lost CAS, a lost claim, a decode failure,
  a denial, a closed window, a passed budget deadline, a disconnected
  client, an unanswering hub and a crash all end in the same in-band
  refusal.
- **Asking another process a question never kills the asker.**
  `process.call` exits its *caller* on timeout rather than returning an
  error, and both questions this package asks on a parked call's own
  effect process — `gateway.attached` once a second for the length of the
  park, and `escalate.borrow` once per refusal — would therefore turn a
  merely slow answer into a dead tool call, settled by the driver as a
  death with no stated reason where the seam's doc promises an in-band
  policy refusal. Both send and select by hand instead, watching the
  callee's monitor: absent, dead, dying mid-answer and too slow all
  degrade to "nobody is there", which un-parks the call and settles it.
- **Compaction is answered by these seams, with the strand's own notes,
  and no provider is asked.** `compaction_hooks` answers every
  compaction's structural decision `VerdictSupplied` with the checkpoint
  `client/checkpoint` builds — the window header, the `agent/{strand}/`
  cells, the operator's instructions — and no usage, because nothing was
  billed. It declines a branch summary (notes say nothing about an
  abandoned branch, and nothing asks for one) and a checkpoint whose
  registers would not read (a declined threshold compaction leaves the
  run alive; publishing a false "no notes" would not be recoverable).
  `VerdictGenerate` is never selected: the machine's generate path is a
  frozen contract the simulation still drives, and a `SummaryRequest`
  reaching `dispatch` is refused terminally, since no prompt exists to
  make one with. The M3 demo installs *these* hooks over its scripted
  provider — it has no `demo_hooks` of its own — so the `CompactionEntry`
  it asserts on is the hook-supplied checkpoint production code
  published.
- **The reminder and the tool read the threshold's own numbers.** The
  `context` slot appends the notes reminder once a strand's context
  passes `checkpoint.reminder_point` — one reserve below the cut — and
  `context_remaining` answers from `checkpoint.remaining_seam`; both
  re-project the strand through `runtime/hooks.project` and price it
  with `hooks.context_tokens`, never from the message list in hand,
  because the fold has to skip what a compaction carried or it reads the
  pre-compaction usage and fires forever. The window is
  `wiring.strand_window`'s, the strand's own. Asked and told are one
  number.
- **The reminder is transient.** It is a transform on one request's
  messages, never an entry: a crash re-projects and re-decides, and the
  tree never holds it. `a_near_limit_request_carries_the_notes_reminder_test`
  pins both halves — that the request carries it inside the band and
  that the durable projection does not.
- **Role follows identity, and the role is derived at dispatch.** An
  `effects.RequestSpec` carries no strand name and one wiring config
  serves every strand, so `wiring.request_target` asks the *captured
  identity* which role it is on: the first routable role in canonical
  order (`main`, then `subagent`) whose usable chain **head** equals it,
  ties to `main`. A match dispatches `ForRole`, so the gateway walks that
  chain inside the one attempt and a rate-limited head costs a fallback
  rather than the machine's retry ladder. No match dispatches
  `ForResolved` on exactly the captured identity. Both answers are a pure
  function of durable state and boot configuration, which is what makes a
  post-crash re-attempt choose what the original attempt chose — recovery
  orphans an in-flight request and re-attempts it, it never re-dispatches
  one. There is deliberately no per-request rerouting, no mutable role
  registry, no health tracking and no sticky chain position: the head is
  tried first, every time.
- **A deferred poll is always `ForResolved`.** The handle belongs to the
  identity that minted it and ORCH-L4 validates the settlement against
  exactly the captured `{provider, model_id, api}`, so a poll that walked
  a chain would fetch a continuation nobody issued. The mirror cost is
  written down in `protocol-change/009`: a *generation* settled by a
  fallback target that returned `Deferred` fails the same check and drains
  as failure. Nothing settles `Deferred` today.
- **A catalogue entry's `thinking` seeds a strand; it never overrides a
  dispatch.** The per-turn level is absolute at dispatch and travels as
  the walk's overlay onto *every* target attempted, so a fallback cannot
  answer at a smaller reasoning budget than the head was asked for.
  Where the entry's level does take effect is strand *creation*, at all
  three points and by one lift (`wiring.strand_thinking_level`):
  `serve.seed_thinking` for `main`, `gateway.seeded_thinking` for
  `fork`/`create_strand`, and `agency.child_configuration` for a spawned
  child. `set_config model_name` deliberately leaves `thinking_level`
  alone — switching model is not a request to un-raise a budget somebody
  raised.
- **A spawned child's model is chosen once, at creation.**
  An explicit `SpawnRequest.model` selects a catalogue identity and its seed
  thinking from `agency.Config.models`; an unknown name refuses before any
  child state is written. Omission uses `Config.subagent_model`, which
  `client/serve` fills from the gateway, or inherits the parent if unrouted.
  The durable seed precedes the brief, so recovery with only that seed or
  with a completed lineage cell never resolves the choice again. The spawn
  receipt reads the child's current configuration. Existing role fallback
  and vision routing still apply to later requests.
- **Model facts follow the identity, not the configured role.**
  `wiring.Config.facts` is an `identity -> #(ResolvedModel, api)` seam
  `client/serve` builds from the catalogue, and it is what makes a strand
  switched off-route honest: admission is answered **per query** from the
  query's own configuration, the compaction threshold's window is the
  *strand's*, and an off-route dispatch target carries the switched-to
  entry's counts. The api half matters as much as the counts — it is
  captured durably into the generation intent and is what ORCH-L4 later
  validates a deferred handle against, so a boot-frozen answer named the
  main entry's dialect for a strand switched to the other one. Only an
  identity the catalogue does not know falls back to `Config.api` and the
  two `fallback_*` counts.
  For `OpenAiResponses`, `serve.adapter_api` uses the adapter's
  `"openai-responses"` constant for both the resolved settings and
  `catalogue_facts`. The client listing carries the distinct dialect through
  its existing open string; neither the wire version nor the durable
  `{catalogue-name, model_id}` identity changes.
- **Subscription profiles select credentials, never endpoints.** Catalogue
  validation requires `auth = "codex"`, bounds the profile name, and refuses
  API-key and URL fields. `codex/transport.validate_request` admits only the
  relative `POST /responses` shape with its fixed non-secret headers. The
  native profile owns token and signed subject binding; the bearer reaches
  only the fixed public Responses HTTP authorization boundary. Control events,
  durable messages and logs carry no token or account subject. Missing plan
  permission is explicit; an identity-only login cannot authorize inference.
  Native and installed Codex CLI credential stores remain separate.
- **The native subscription worker lives through HTTP drain.** The HTTP owner
  monitors the worker that prepared it even after begin. `codex_bridge` therefore
  monitors that owner before begin and keeps its creating worker alive until
  the owner exits. Its surviving Weft scope independently retains cancellation
  and drain proof; a normal worker return cannot cancel a still-admitted stream,
  and an abnormal native exit cannot become a clean scope exit.
- **A checkpoint never publishes a blank.** A strand with no notes is
  told, in the checkpoint, that it wrote none and where notes go; a
  checkpoint whose inputs would not read is declined rather than
  rendered from an empty board. Neither shape can replace a window with
  nothing, or with a false record of nothing.
- **The `before_compact` note lands in the checkpoint.**
  `runtime/strand_runtime` asks `Hooks.compaction_note` at the structural
  decision and appends every block after the harness's own text and the
  strand's notes; `extension/hooks.note_block` frames it for the model
  reading its next window, not for a summarizer. Still never a veto, and
  still under the replay rule: the decision is transient until the
  publication commits it.
- **The operator's `compact` instructions reach the checkpoint from the
  operation's durable state**, not from the preparation:
  `StructuralPreparation` has no field for them, because the preparation
  is the *input* the decision hook froze and the instructions are a
  property of the operation that asked. They are quoted in the
  operator's own words, in their own fence.
- **A manual `compact` cuts where an automatic one cuts.** The hub's
  preparation goes through `runtime/hooks.preparation` — the same
  builder the threshold and overflow hooks use — against the run's own
  settings snapshot.
- **The server has two supervision tiers, and the line between them is
  reachability.** A child under `Instance.services` — the commit
  forwarder, the Agency holder, the escalation holder, the scratch
  store, the rule scanner, the scheduled-heartbeat scanner, the
  search-index holder and its commit subscriber, the gateway hub —
  is addressed by
  a reclaimable reference, so a replacement binds the same address and a
  crash costs hints and the sockets already attached to the old hub
  rather than the server. The helper pool, the broker, and the summary
  sink are captured *by value* into closures built during the boot, so a
  replacement would be unreachable by everything already holding the old
  handle: restarting one leaves a server that looks alive and refuses
  every call, and failing closed is the posture the effect plane wants
  anyway. Those are fatal, along with the session tree, the listener,
  and the service supervisor once its own budget is spent.
- **A plane nobody configured costs nothing, and says so.** A
  `loom.toml` with no `[[rule]]` starts no scanner at all — not a
  scanner holding an empty list — and logs `rules.none`; a configured
  one logs `rules.loaded` with the count and the names. A `loom.toml`
  with no `[[schedule]]` takes the identical posture: `schedules.none`
  or `schedules.loaded`, no scanner started or a supervised one holding
  exactly the configured list. This is the `codemode.unavailable` line's
  reasoning applied again: an operator who configured something and
  sees no effect must have a line to reason from, and a host that
  configured nothing must run exactly the processes it ran before the
  feature existed.
- **A dormant rule is byte-absent from a context, not merely cheap.**
  Rules live in the configuration file, fired-marks and cursors live in
  reserved registers, and neither is an entry — so nothing a rule
  contains can reach a projection until the scanner commits an
  injection. The projection tests assert exactly that, in both
  directions.
- **Recall is a projection with no authority, and the wiring says so
  three times.** An index that will not open costs the `history_search`
  tool and one log line, never the boot. A holder that crashes is
  restarted and reopens the file. A sync that fails leaves the durable
  cursor where it was, so the next commit's poke retries it. Nothing
  about a session's correctness depends on the index being right, which
  is what lets every one of those failures be quiet.
- **The distillation pass runs once per boot, skips the live session by
  its lease, and retries by being run again next boot.** Each clause is
  load-bearing. *Once*, because the material it may read does not change
  while this server runs — a live session's own lease is what puts it out
  of reach — so a timer or a per-turn hook would buy re-reads and model
  turns for nothing. *By the lease*, because the worker starts in the
  service tier, after `assemble` has taken the session's writer lease, so
  the skip is a property of the ordering rather than of a check. *Next
  boot*, because the pipeline's write order (rows, head-and-cursors CAS,
  sidecar) means a pass killed anywhere moved no cursor. The one residue
  is the memory lease a killed pass cannot release: the store is
  consistent, and a boot inside the ten-minute run TTL logs
  `memory.distill.failed` naming the holder rather than distilling.
- **A fatal death is an orderly shutdown, not a side effect.** The host
  traps exits, so a child's death is a message: it runs the whole
  teardown — listener, services, runtime (which releases the writer
  lease), broker, pool, sink — *before* reporting, and the entry point
  exits nonzero afterwards. The one case this cannot cover is the
  storage actor's own death, because it is the connection that would
  delete the lease row.
- **Only the host tears a booted stack down.** `serve.shutdown` asks the
  host (`host.retire`) and waits for its exit; it never runs a teardown of
  its own. Two concurrent teardowns cannot both hold the runtime's drain
  witness, and the one without it returns before the other has released
  the lease, which is how a boot straight after `shutdown` once found the
  old incarnation's lease. `open_instance` still leaves the close to its
  caller, which is safe only because nothing that close stops before the
  runtime is a fatal child.
- **A session open outwaits a distillation harvest.** A harvest opens a
  source session under the ordinary writer lease as
  `distill.distill_owner`, and the daemon runs one beside admission by
  design. `serve.open_session_file` retries a `LeaseHeld` naming that
  owner until the harvest closes or its lease (at most
  `memory.lease_ttl_ms`) expires, and logs `session.harvest_wait` once.
  Any other holder is refused at once, as before.
- **The entry point has two output channels, and the split is
  deliberate** (§3.4). **stdout** carries the startup banner and nothing
  else — the ephemeral port, the token path, the prompt digest — because
  that is this process's contract with whoever launched it and a
  supervisor script reads it with `head -1`. **The log stream** carries
  everything about the running system, as JSON, one event per line, all
  of it correlated to the session. A *usage* error is on neither side of
  that split: it belongs to the person who mistyped a flag, before there
  is a session to correlate anything to, so it stays on stderr with the
  usage text. Everything downstream of a successful flags parse — the
  boot failure, the SIGTERM stop, a fatal child, an absent code-mode
  toolchain, a prompt-pack warning — is a log line.
- **The handler is installed before anything can fail.** `main`'s first
  act is `handler.install`, so no line of a boot lands on the VM's
  default text formatter; `LOOM_LOG_LEVEL` sets the threshold and an
  unrecognised value falls back to `info` rather than refusing to boot.
- **Only an entry point installs the signal handler.** `wait_for_sigterm`
  replaces the VM's default `erl_signal_handler`, whose answer to
  `SIGTERM` is an immediate `init:stop()`, so `boot` never touches it —
  `main` does, through `host.relay_sigterm`, and a test that boots a
  server leaves the node's signal handling alone.

## Shipped fixtures run the daemon with an isolated home

Every fixture that launches the real `bin/loomd` named by
`LOOM_BOOTSTRAP_E2E_SERVER` reads that variable through
`test/support/shipped_server.from_environment`, never through `getenv`
directly. It returns a generated launcher script under
`build/shipped-home-<hex>/` that exports `HOME` as an empty private
directory, `XDG_CONFIG_HOME` inside it, and `GIT_CONFIG_GLOBAL` naming a
generated `Loom Fixture` identity, then execs the real server with its
arguments unchanged. A symbolic link to the server's sibling `loom-exec`
sits beside the script, so the launcher still passes `--helper` and
`test/support/enforcement.probe` still finds the helper. The daemon reads
the operator's `~/.claude/settings.json` hooks, home skills, installed
extensions under `~/.loom/extensions`, home guidance files, and global Git
identity from `HOME`. Before this, a developer's own hooks ran inside
fixture sessions and failed them on macOS, while `signoff/linux` passed
only because its container has no `~/.claude`. The test VM's own
environment is left alone: `git_identity_test` and other suites in the
same VM read it, and `os:putenv` would change it for all of them. A new
shipped fixture takes its server from `shipped_server.from_environment`.

## Shipped recovery observations

The reservation and identity recovery fixtures keep lifecycle mutations on
their original control connection. `test/support/daemon_observation` opens
one authenticated connection per lifecycle wait, using the fixture's original
endpoint and epoch. Its handshake and every `GetSession` read share the same
fifteen-second deadline. A transport failure ends that wait; it never retries
a retired control owner. Closure is requested before the observation outcome
is checked, and no probe launches another daemon. Mutation replies retain
their existing unknown-outcome semantics.

## Closure ownership at process boundaries

`hookserve.wire` binds each prior hook or tool function before composing its
wrapper. Capturing the enclosing `Hooks` or `ToolSurface` would copy unrelated
slots into every supervisor and worker that receives the effects. Likewise,
`wiring.compaction_hooks` captures the session for projection, and
`gateway.tool_output_observer` captures only event identity fields from
`ToolRun`. Their regression tests vary unrelated record contents and compare
flattened callback sizes; the size probe is confined to test support. See
[the daemon memory investigation](../../docs/design-notes/daemon-memory.md)
for the isolated workload measurements and their limits.

The session socket projects a resolved `Attachment` into a private authorization
record before creating its long-lived handler and gateway callbacks. That
record retains the binding, parser permit and registry handle; the resident
instance and runtime effects are no longer transport-owned. Authorization
still rechecks the original epoch, incarnation and credential on the registry.

The value the daemon manager holds per resident session, and replies to an
upgrade with, is `serve.Resident` rather than `serve.Instance`. The manager is
generic in that value and reads none of its fields: it calls only the
assembly's `fatal` and `drain`. So the narrowing is a change of the type the
daemon instantiates, not of the slot lifecycle. `owned_assembly_test` pins the
projection against a real assembly, and `runtime/drain_test` varies an effect
graph and checks that `api.draining` does not grow with it.

The daemon manager projects its command subject and assembly callback before
constructing the persistent instance host's builder. The host keeps that builder
after publication, so capturing the manager book would retain earlier resident
instances. Domain builders likewise capture only their projected callback and
source lookup. `daemon_manager_test` opens three real host pairs with large runtime
payloads and checks that host state stays bounded while publication and ordered
shutdown still succeed.

## Idle assembly actors hibernate

A session's weft actors take `runtime/residency.hibernate_after_ms` — one
constant, thirty seconds — and no site overrides it: `escalate`, `agency`,
`rulescan`, `jobs`, `scratch`, `history`, `internal/shared_history`,
`extension/hosts`, `advisor`, and the counters actor inside `hookserve.wire`.
`erlang:hibernate/3` sweeps and then shrinks the heap block to the live data,
so a parked session gives back the working heap of its last turn.

`runtime/strand_runtime` takes it too, and it is the one worth the most: it
holds the largest `Effects` heap in the assembly. It used to be out of reach
because it re-armed its checkpoint poll every 200 ms unconditionally. The poll
now runs at two periods — 200 ms while an operation is open, two minutes
between turns — so a parked strand's mailbox is quiet for four times the
residency interval while the doorbell-loss backstop is kept at the longer
period. `packages/runtime/CLAUDE.md` has the liveness argument and the reason
the chain only ever replaces itself.

Two kinds of actor are deliberately left awake, and the reasons are
load-bearing rather than oversights. The **gateway hub** ticks
`MaintainTransfers` every second through `actor.periodic`, which is a property
of the actor rather than of a `Next`, so a handler can neither cancel nor
re-time it and the receive timeout can never expire; reaching it needs either a
cancellable tick in weft or a `weft/state_machine` whose periodic timeout
belongs to the step. The **commit forwarders**
(`gateway.commit_forwarder`, `history.commit_pull`,
`history.supervised_shared_commit_pull`) and `serve`'s owned publication child
are stateless `Nil` processes: nothing to reclaim, and a wake sweep would land
on the hot path of every commit. `distillpass`'s domain stop token is
request-scoped and dies with its pass.

Hibernation returns garbage only. The live set it leaves behind is dominated by
this assembly's per-process copies of `Effects`, so this is a complement to
sharing that value and never a substitute; the measured local yield is 3 to 5%
of an idle six-session assembly across runs, and the yield on the installed
daemon is unmeasured. `client@assembly_heap_census_test` observes it — it
confirms `erlang:hibernate/3` through `process_info` rather than assuming the
interval fired — behind `LOOM_ASSEMBLY_HEAP_CENSUS`, and with a
`scripts/serial-tests` line because it differences `erlang:processes/0` and
suspends what it finds.
[The daemon memory investigation](../../docs/design-notes/daemon-memory.md)
carries the tables and the rerun command.

The advisor publishes `events/bus.GoalChanged` on `Outputs` only after
`put_reserved_fact` or `delete_reserved_fact` successfully returns for
`advisor.goal_key`. Guard and cursor writes publish no goal invalidation.
The gateway relays it as an uncorrelated `goal_changed` with an empty body,
through the same subscribed-peer delivery and authorization as other pushes
(protocol-change/056). A phase edge is not evidence that the asynchronous
goal evaluation has written its cell. `goal_e2e_test` holds the review's
provider response until the primary settles and an attached client reads
Active, then proves that satisfaction is notified without a primary wake.

## Held input and goal delivery reading path

For the #583 paths merged at `8b3455493`, the module `## Flow` comments name
local entrypoints and their owned helpers. `advisor.store_goal` and
`clear_goal` reach `write_cell` and `delete_cell`; only their successful
writer replies reach `goal_written`. `gateway.handle` relays the notice through
`push_to_subscribed` and `deliver`, preserving subscribed-peer authorization.
A bounded goal read refusal can keep the connection attached, so clients
must invalidate the observation on the correlated refusal itself.
The [delivery reading guide](../../docs/architecture/delivery.md#reading-the-held-input-and-goal-paths-in-gleam)
connects these paths to held queue projection and shared request ownership.

## Deep Docs

- [docs/architecture/codex-subscription.md](../../docs/architecture/codex-subscription.md)
  — the opt-in helper, credential, request, and drain boundaries and the
  remaining live interoperability and support gates.
- [docs/architecture/orchestration.md](../../docs/architecture/orchestration.md)
  — the runtime surface the hub dispatches onto.
- [docs/architecture/durability.md](../../docs/architecture/durability.md)
  — seqs, write-once rows, and why the event stream needs no side index.
- [docs/spec-gaps.md](../../docs/spec-gaps.md) — "From WP-L (`client`)":
  the historical client gaps and their dispositions, including the now
  shared compaction/navigation admission and brief-less strand creation.
- [protocol-change/007-escalation-carries-the-action.md](../../protocol-change/007-escalation-carries-the-action.md)
  — why `escalation` carries the tool, the action digest and a bounded
  argument preview, why `approve` echoes them, and the rendering rules
  that bind any client showing a preview.
- [docs/design-notes/agent-comms-and-system-prompt.md](../../docs/design-notes/agent-comms-and-system-prompt.md)
  — Part B: the pack's sections, the stability contract, and why
  the prompt is pinned rather than re-derived.
- [docs/review/m5-agent-comms-judgment.md](../../docs/review/m5-agent-comms-judgment.md)
  — change items 5 and 6, which the sandbox wording and the
  pin-around-the-lease ordering follow.
- [docs/architecture/code-mode.md](../../docs/architecture/code-mode.md) —
  the pipeline `client/codemode` wires, and what each layer confines.
- [packages/codemode/CLAUDE.md](../codemode/CLAUDE.md) — the pipeline's
  own package.
- [docs/architecture/memory.md](../../docs/architecture/memory.md) — the
  store, the pipeline, the lease rules, the lifecycle worker, the digest
  injection and the erasure cascade's rewind-on-drop (#124).
- [docs/design-notes/compaction-and-memory.md](../../docs/design-notes/compaction-and-memory.md)
  — Stage C0: which seams were inert, what each hook now decides from,
  and the cache arithmetic the summary request's shape follows.
- [packages/prompt/CLAUDE.md](../prompt/CLAUDE.md) — the pure half:
  the pack format, the renderer, the summarization pack, and what
  `Environment` may never grow.
- [docs/architecture/advisor.md](../../docs/architecture/advisor.md) — the
  advisor strand: why it is a peer rather than an Agency child, the feed's
  cadence and its coalescing backpressure, the three verdicts and the guard
  that rations them, the two cells, and what is deferred.
- [packages/tui/CLAUDE.md](../tui/CLAUDE.md) — the other end of the wire.
- [Root CLAUDE.md](../../CLAUDE.md) — repo ground rules and the doc graph.

The worktree observer enumerates tracked status separately from untracked
files. Its untracked query excludes only Loom's canonical generated
`.codemode` directory and the legacy `.blobs` directory before enumeration,
so toolchain caches cannot consume the observation budget. Nothing writes
`.blobs` any more; the exclude covers workspaces an earlier release used.
Tracked changes under those directories still appear. Both outputs retain NUL-framed path identity and the existing
read-only broker, deadline and byte bounds; no repository ignore file is edited.

Session assembly keeps `.codemode` out of the operator's own
`git status` without editing a repository file either: `prepare_directories`
writes `workspace_policy.ignore_everything` as `.gitignore` inside each, where none
exists. An ignore file inside an untracked directory hides the directory
itself, so no git metadata is resolved, and a linked worktree needs nothing
different. An operator's replacement file is left alone. `$TMPDIR` under
`.codemode/tmp` is therefore the persistent scratch the `bash` description
names: a clone or a download a later call reads goes there, never into the
repository and never into `LOOM_SCRATCH_DIR`, which the helper removes when
its execution retires.

## Child run lifecycle

The Agency keeps immutable `lineage.Lineage` for addressing and spawn replay,
while `runtime/child_run.Run` stores each operation's owner, deadline and stop
cause. `current_peer` selects active or latest work; old handles still read
operation-keyed results. `send` delegates idle admission to `api.send_to_child`,
which commits budget and owner with the new operation. Explicit budgets are
refused for busy children. Parent reaping matches the current run's owner and
records its cause before requesting an exact-operation abort. New cancellation
decisions do not update legacy `Lineage.reaped`. Proposal 042 records the wire
and storage contract; `docs/review/child-run-lifecycle.md` records the remaining
schedule and legacy held-rule policies.

## Connection admission configuration

`daemon/limits` validates positive `max_connections` and
`max_reserved_message_bytes` settings in the same `[daemon]` table as optional
`profile` and boolean `ui` (default false). Both `catalog` and daemon startup
use that parser. `daemon/main.prepare_startup` captures the web-view choice
and connection limits from one read; `--ui` enables the view even when the file
says false. The production entrypoint carries the resolved configuration into
UI construction and listener publication. `daemon/main`
loads the last explicit startup configuration before preparing the root;
session-specific configuration cannot alter a live root's limits. The root
captures the limits in its handle and accounting state, and the control hello
reports those captured values. Count and byte refusals name the exhausted
setting before parser activation. Existing transfer, cancellation and DOWN
accounting remain the owners of capacity release.

## Async collaboration (protocol 048)

`async_runs` is a session-owned weft actor. `Launch`, `Inspect`, `Reported`,
`Sweep`, `Recover` and `AbortOperation` manage durable execution records and
volatile scopes. `async_codemode` captures source, policy, grants, a fixed
deadline and a distinct broker step. `execution.receive` reads the bounded
durable input journal. `Finished` requires scope and owned-child drain proof;
restart records `Lost` and retries child cleanup without replaying a program.
Once a terminal phase is saved, the service sends the launching strand a
`client/notice` completion notice, except for an owner's cancel, an
operation abort and a session stop. `Wiring.heartbeat_ms` (the jobs
policy's) drives the same minute-sampled idle heartbeat the jobs actor
runs, over `Starting` and `Running` executions.

Agency serializes `SpawnChild`, `WorkflowChild` and `PeerRequest`; `Borrow`
keeps blocking waits outside that mailbox. `workflow_ledger` fixes a named
run's version/input and a named step's assignment/call site before admission.
`workflows` binds the current async custody. The immutable initial child
operation is committed with admission, so interrupted lineage publication
cannot select a later unrelated run.

`peer_mail` owns exact directional grants and atomic receipts in reserved
`client/peers/` facts. `peers` exposes `peer_roster`, `peer_send`, `peer_describe`
and the cap router. `daemon/main.peer_directory` resolves only residents and
reads saved metadata without opening stores. `Assembly.build` receives the
small manager handle; resident values carry an address-only peer endpoint.
The owner/epoch-checked `peers.link` and `peers.unlink` controls are the only
grant mutation surface. Owner-only `peers.inspect` reads one resident strand's
outgoing links and recipient-owned incoming grants. The `Grants` Agency command
returns incoming wake policy, while outgoing rows combine catalogue metadata
and the exact target strand's wake policy. Inspection sorts both directions
under one opaque cursor and keeps every serialized reply below 60,000 bytes;
pages are fresh observations. Unavailable recipients remain visible without
opening saved stores. Owner-only `peers.send` reuses the normal sender and
recipient handlers after checking the daemon epoch. It selects a resident source
strand but cannot bypass links or supply provenance metadata. Communication
grants confer no lineage or custody.
`peer_mail.Link` enforces 64 outgoing links per source strand before writing the
source index. Replacing an exact link at the limit remains idempotent.

Default peer links (protocol-change/077). `client/peer_defaults` decodes the
`[peers]` table (`default_links = "off" | "same_owner"`, `default_wake`),
strictly, and `catalog` validates it with every other table; `daemon/main`
captures it once at startup in `Config.peer_policy` and never rereads it. The
daemon hands each session `peer_mail.Defaults(policy, eligible)` through
`serve.Settings.peer_defaults` and `agency.Config.peer_defaults`; `Agency`
calls `peer_mail.handle_with`, and `handle` is the same with `no_defaults`.
`eligible` is `manager.unshared_sessions`, a registry read of the active
sessions with no membership row (resident or not, at most 256, from one `access.membered_sessions` query and the catalogue pages), evaluated at
each admission and listing. `peer_mail.implicit_wake` is the only place a
default link is decided (same_owner, both strands `main`, different sessions, no
denial, both sessions eligible), and delivery, `Roster`, `Links` and `Grants` all
use it: an explicit grant decides alone, a default delivery commits under the
guard that neither a grant nor a denial exists. `Revoke` and `Unlink` record
`client/peers/denial/<digest>` for a `main` to `main` pair and `Allow` and `Link`
clear it, each in one `api.edit_reserved_facts` transaction. Default rows carry
`default: true` in `Links`, `Grants` and `peers.inspect`; roster rows carry
`running`. `peers.send` queues a send to a closed recipient that its owner holds
(`peer_mail.NotOpen`, the next paragraph and the peer mail reach paragraph below)
and refuses one that no catalogue holds with `peers.not_running`; nothing opens a
session. Default entries share the 64-link bound after the explicit ones. A new `[peers]` key means `peer_defaults` and its test.

Sender outbox (protocol-change/078 addendum). `peers.send` records each message
in the sending session before it asks the recipient: `peer_mail.OutboxClaim`
writes `client/peers/outbox/<digest(strand, session, id)>` `pending`
(`put_reserved_fact_expecting`, expecting absent), the inline attempt runs, and
`OutboxSettle` flips the row to `admitted` (receipt) or `refused` (reason), and
rewrites a `pending` row only when its `wait` (`OnOwner` | `OnOpen`, stored as
`wait: "owner" | "open"`, absent in an older row and read as `OnOwner`) changes. An
unreachable owner is the typed failure `peer_mail.Unreachable` (`Endpoint.call`
and `Directory.resolve` answer `Result(_, peer_mail.Failure)`, where `Refused(reason)`
is a definitive answer), which a local endpoint never returns; `peers.send` then
leaves the row `pending` and returns
`{"state": "queued"}` (`peers.queued`, `peers.is_queued`), and the router
answers a program with the denial `peer_queued`, carrying the answer's own note,
because `cap/peer.send` is typed to a receipt. `peer_mail.NotOpen` is the third
failure: the owner holds the session and it is not resident. It is queued the same
way, as `peer_outbox.NotOpen` and `peers.queued_unopened` with
`peers.queued_unopened_note`. `client/peer_outbox` is the pure module (row, `claim`, `room`,
`settle`, `expire`, codec); `client/internal/peer_outbox_store` applies it to the
store inside the Agency actor; `OutboxDue` also refuses rows pending over an
hour, with `peer_mail.unreachable_reason` for a row that last waited on its owner
and `peer_mail.not_opened_in_time_reason` for one that last waited for an open
(`peer_outbox.expiry_reason`). `client/peer_outbox_drain` is a
`weft/state_machine` (`Idle`/`Waiting`) with one named timeout
`peer-outbox-drain`: a pass at session open, then every `retry_interval_ms`
(5000) while any row waits on an owner that did not answer, and a doubling
interval up to `max_retry_interval_ms` (300000) while every owed row waits for an
open (`backlog_of` decides from the list of outcomes, so the order of the rows
does not matter; a session that did not answer or is saved is asked once per
pass; `State.interval_ms` is the interval in force), no timer when none
is owed. The doorbell cannot say which row it announces, since the machine's own
settles ring it, so a new row waits for the next tick. It is
built from a `peers.Wiring` and an injected `after`, reads rows through
`OutboxDue`, and attempts each through `peers.resend`, which resolves through
`Directory.resolve` like `send`. `agency.Config.outbox_queued` rings it
(`peer_outbox_drain.poke`) after an `OutboxSettle(Unanswered | NotOpen)`; `serve` wires the
closure and adds the drainer beside the schedule scanner. Bounds: 64 rows per
strand (`outbox_full` when all pending, else the oldest finished row is evicted),
one hour pending, `Unlink` deletes the pending rows to the link,
the `peer.sent_receipt` router arm reads an admitted local row first. A refused row is replaced
by a repeat of its id. The drainer is not labelled with a `telemetry/owner` role
(roles are frozen by the protocol), so an ownership inspector reports it as
`unknown`. Tests: `peer_outbox_test` (pure), `peer_outbox_flow_test` and
`peer_outbox_drain_test` over `support/peer_rig` (two real runtimes behind a
scripted network, a fake timer wheel, a file-backed reopen; the rig's directory
answers `Resident`, `Saved` (`NotOpen`) or `Gone` (`Refused`), and any other
session is saved).

An owner's web page manages links through `daemon/ui_peers`: `run` executes a
`web_view/peer_links.Request` against `peers.inspect`, `peers.link` and
`peers.unlink_session` (also what the `peers.unlink` control calls), judges strands
and session identities again, resolves only resident sessions, and maps a refusal
to a fixed `Reason`. `ui_socket.peer_links_for` re-derives the page's owner at
each request, `peer_links_capability` hands the capability to an `Owning` page
alone, and `owner_accepts` alone admits events beneath `component.peers_path`.
`server.Attachment.peers` is the daemon's `peers.Directory`.

Epoch-checked `sessions.activity` (`protocol.SessionActivity`)
takes 1 to 24 distinct canonical ids and reports what each resident is doing.
The owner is answered for any id; a member only for ids its credential holds a
membership in (`held`), the rest dropped as unknown ids are.
`server.activity` resolves each id through `manager.resolve`, which answers only
for `Running` slots, so saved or unknown ids are omitted and no saved store is
opened. It then calls the read-only `peer_mail.Overview` command on every
resident concurrently from the control socket's process, in one `weft` run
under a 2,000 ms deadline; any outcome other than an answer becomes a
`{"session_id","state":"unknown"}` row. `Overview` runs in the recipient Agency
actor and derives `state` (`needs_you` for a pending escalation or a failed,
stopped main; else `working` when any strand has a current operation; else
`idle`), counts, main's last run outcome, final assistant text, model, and at
most four glances whose operation is still current. The recipient bounds its
row to `peer_mail.overview_row_bytes` (2,300) by shedding glances, then the
message; the server re-checks 2,400 bytes per row and 60,000 per reply.

See [async collaboration](../../docs/async-collaboration.md) for bounds and
recovery semantics, [Protocol 049](../../protocol-change/049-peer-inspection.md)
for the read-only owner inspection extension, and
[Protocol 050](../../protocol-change/050-session-activity.md) for session
activity.

Input readiness is separate from the execution phase. `Ready` publishes an
immutable endpoint set and idle interval; `SendTo` journals a named value only
after that endpoint is ready. `ReceiveEnveloped` returns its sequence, endpoint
and value for satellite-side typed dispatch. Legacy `Receive` registers raw
`default` readiness. `Progress` and `Delivery` update bounded volatile
observations, exposed with lifecycle through `Check` and lost on service restart.
The eight-live session ceiling is complemented by 32 cumulative launches per
initiating operation, reconstructed from durable records during recovery.
Typed idle expiry closes admission and reaps the execution; successful callback
delivery alone resets its idle anchor. The fixed wall deadline remains separate.

`peer_mail` stores plain message text with `PeerOrigin(session, strand)` bound
by the sending harness. The receipt separately retains observed metadata.
The entry codec and provider projection preserve peer attribution after replay.
See [async architecture](../../docs/architecture/async-collaboration.md) for
custody, readiness, delivery and exclusive invocation boundaries.

The Agency's `frame_message`, `frame_brief` and `child_notice` build their
head, foot and contract lines from `session_view/strand_framing`, the one
definition the hosts also strip. Protocol-change 059's reader draws `StrandOrigin` and the writer sets it
in the same release: `agency.brief_message` and the
`agent_send` payload carry `Some(StrandOrigin(caller.strand))`, set from the
authenticated caller and from nothing a model emitted, with the framed text
unchanged. The origin is attribution only. `vision.collect_turn` ends the
current turn at such a message as it does at a peer message, and
`wiring.admitted_image_bearing` still protects an image admitted earlier in
the same run; `vision_test` pins that image-then-sibling order, and
`agency_test` pins the origin for briefs (with and without a result schema),
downward sends and upward reports.

MCP layer retirement fixes one monotonic proof deadline before issuing stops.
Each parallel collector passes only the remaining budget to client shutdown;
late scheduling cannot grant a fresh per-client wait. The outer Weft scope
retains its collection margin so a verdict at the proof cutoff can be observed.

## Language servers

ADR-015 is the ruling, and ADR-016 §§1–2 makes a server's table a
language profile; these are the pieces that carry both in this package.

No language is built into this package. The first-party profiles for Gleam,
Go and Rust are extensions in their own repositories
([loom-lsp-gleam](https://github.com/Roasbeef/loom-lsp-gleam),
[loom-lsp-go](https://github.com/Roasbeef/loom-lsp-go),
[loom-lsp-rust](https://github.com/Roasbeef/loom-lsp-rust); ADR-016,
addendum), installed with `loomd ext install <url> --rev v0.1.0` and proved
with `loomd ext check`. The tests here use synthetic profiles, and a name
such as `lsp_go` in them is a fixture's, not a dependency on that repository.

- `client/lsp/profile.{LspServer, ProjectAccess, LspPath, ModuleCase,
  Places, decode_servers, decode_server, claim_extensions, expand_path,
  cache_place, cache_env_paths, private_cache_fault, mangling_fault,
  not_server_owned}` — the one
  `[lsp.<name>]` decoder, pure (no I/O, no external). `client/catalog`
  hands `decode_servers` the `[lsp]` table's entries, and so does
  `client/extension/manifest` for a profile extension, so the two can
  never accept different things. `encode_server` and `server_decoder` are
  the profile's JSON form in an install record (every field written,
  roots as written, the decoder total and structural: the load's
  comparison with the re-decoded manifest is what holds the table's
  rules), and `approval_lines` is what an install prints for one: every
  key, plus the two grants the jail derives (the executable's directory,
  read-only, and each private cache). `LspServer` carries the ADR-015
  keys, `cache_env: List(#(String, String))` (variable to a relative
  directory, sorted by name; `cache_env_paths` places each at
  `<cache>/loom/lsp/<server>/<dir>`, the one value-carrying environment a
  profile may set; a name follows the `env` rules and is not also in
  `env`, a directory has no empty, `.` or `..` component and no leading
  `/` and lies inside no other entry's, and the record decoder re-reads it under the same rule; an absent
  `cache_env` in a record reads as `[]`, since format 3 predates the key), plus four
  profile keys, each defaulting to the behaviour it replaced: `language_id: String` (always filled; default the first
  extension without its dot; written, `[a-z0-9][a-z0-9+._-]*` in at most
  40 characters), `qualifier_separators: List(String)` (default `["."]`;
  each non-empty, no whitespace, not `/`, listed once; `[]` refused),
  `module_case: ModuleCase` (`AsWritten` | `Snake`, from `"as-written"` or
  `"snake"`), and `hint: Option(String)` (one line, no control character,
  at most 200 bytes). `LspPath` is `AbsolutePath` | `HomePath(rest)` |
  `CachePath(rest)`, the last written `<cache>/rest` under the `~/` rules.
  **Invariant: no root names Loom's private cache.** A `readable` or
  `writable` root written `<cache>/loom[/...]` (compared by component, so
  `<cache>/./loom` too) is refused at decode, record decode included, and
  `private_cache_fault(server, places)` — called from
  `workspace_policy.lsp_server_roots` at boot and by `loom ext check` — refuses an
  absolute or `~/` root that resolves inside `<cache>/loom`, or a
  `writable` one that holds it (`~/.cache`): each would let a server swap
  a private cache for a link.
  `expand_path(path, Places(home:, cache:))` resolves both relative forms
  and refuses a missing or relative place; `cache_place(os, home,
  xdg_cache_home)` is the pure platform rule (`darwin`:
  `home/Library/Caches`; else an absolute `XDG_CACHE_HOME`, else
  `home/.cache`). `mangling_fault` and `not_server_owned` live here
  because `catalog`, which imports this module, shares them for its
  `[mcp.<name>]` and `[tools]` checks.
- `client/lsp/profiles.{Claimant, Conflict, Refusal,
  effective_lsp_servers, describe_claimant, describe_conflict}` — ADR-016
  §4, pure. `effective_lsp_servers(configured:, installed:)` returns the
  session's servers sorted by name plus the refused installed profiles.
  **Invariant: the operator's file wins whole, and conflicts refuse every
  installed profile involved, never first-wins.** A `loom.toml` table
  replaces an installed profile of the same name (not a refusal); every
  remaining installed profile is judged against the whole candidate set,
  refused for sharing a server name with another installed profile or a
  file extension with any server, so the answer does not depend on
  discovery order. A `loom.toml` server is never the refused side.
- **Invariant: a profile key's default is the old behaviour.** A table
  naming none of the four keys decodes to exactly what ADR-015 shipped;
  `catalog_lsp_test` pins it, and the resolve tests pass with `["."]` and
  `AsWritten` as they did before the parameters existed.

- `client/lsp/profile_check.{CheckOutcome, run, run_all, passed,
  describe}` — a profile's `[[check]]`s through a `query.Door`, and no
  other effect. `run(door, check)` asks `definition` or `references` with
  `SymbolQuery(symbol, path, line)` and answers `Passed`,
  `Mismatch(expected, got)` (both sorted, unique `path:line` lists) or
  `Errored(reason)`. **Invariant: sites compare as sets**, since a server
  answers in no fixed order and two references on one line share a
  `path:line`; `profile_check_test` pins it.
- `client/lsp/manager.{Manager, Config, Backend, Timing, Jailed, Search, Hit,
  Msg, start, supervised, addressed, stop, door, jailed, connect_jailed,
  probe, probe_server, search_jailed}` — the session's one-server manager.
  `probe_server(jailed, server, root)` clears the same probe over the
  same jail and answers its `CallOutcome` unjudged, for `loom ext check`'s
  jail line. The door's
  closures run in the caller: they ask the manager for the live client,
  then do the pull-resync, the requests and the conversion to `Site`s
  themselves, so a slow query holds up only its caller. The probe
  (`probe_argv` under the server's exact policy) must pass under the
  session's demand before the server clears; `after_write` never starts a
  server. `supervised(name, config)` is the service-tier child (transient,
  bound to a `weft/registry` address); `addressed(name, config)` is the
  handle over it, resolving the address per exchange, so a replacement is
  the same manager to every door. `stop` asks the manager to shut down and
  waits, in the caller and bounded by `Timing.previous_ms`, for the
  server's keeper to finish its graceful stop.
  The module doc carries a `## Flow` spine and the transition tables for
  the manager's `Phase` (`Idle`, `Starting`, `Running`) and the keeper's
  `KeeperPhase` (`AwaitingPrevious`, `Beginning`, `Holding`); the state,
  message and keeper types sit before the first function.
- `client/lsp/resolve.{Identity, Owned, Unowned, Symbol, owner, admit,
  same, display, split_symbol, cased, satisfies, named, container,
  outline, site}` — the pure half: which `{server, root}` owns a path
  (nearest root marker, real path under the root; an absolute path is
  placed under the workspace as written or its real location, since the
  write observer hands over real paths), which server-named paths the
  harness may read (`admit`), how a symbol splits into qualifier and
  identifier (`split_symbol(symbol, separators)`, longest separator
  first, segments joined with `/`), which definition a qualifier selects
  (`satisfies(root, path, qualifier, module_case)`, the module-path match
  `cased` to snake_case under `Snake`; `named` compares an outline parent
  chain with the qualifier as written), and how a location renders as
  `path:line:anchor|text`. The manager splits a symbol only once it knows
  the owning server: after `owner` for a path, per server for a bare-name
  search. A document's `languageId` is `LspServer.language_id`.
- **Invariant: no server-named path is read, opened or echoed ungated.**
  The jail bounds what a server reads, never which paths it emits, and the
  door reads in the caller, unjailed. Every path out of an answer
  (definition, references, call edges, published diagnostics, a rename's
  `WorkspaceEdit`, bare-name hits) passes `resolve.admit(root:, protected:,
  path:)` — `tools/fs.resolve_writable` against the server's real root and
  `Backend.protected` (the session base's list, filled by `jailed`) — and
  becomes `Admitted`/`Withheld`. Withheld: shown at raw coordinates with
  `text: ""`, never `didOpen`ed (`resync` gates again), and a rename naming
  one answers `ServerRefused` whole. The bound is the root alone, not the
  server's `readable` roots, so a stdlib jump shows no line text.
  `resolve.read_text` has no discipline of its own; its callers pass
  `owner` or `admit` output only.
- **Invariant: one lapsed request ends a batch.** A bare-name search's
  `definition` fold answers `Unavailable` at the first `TimedOut`,
  `Unavailable` or `Unsupported`; the references `documentSymbol` fold stops
  outlining there and keeps the rest with no container. Neither holds a
  caller for one deadline per hit.
- **Invariant: the query that starts a server waits for its load, and
  no other query waits.** When a query's `Warmth` is `Started`, after the
  pull and before its first request it calls `lsp/client.ready` with
  `Timing.quiet_ms` (300 ms), bounded by `Timing.ready_ms` (60 s).
  `StillBusy(titles)` is `query.Unavailable("the language server is still
  loading (<titles>); ask again in a moment")`, never an empty answer
  (`rust-analyzer` answers mid-load with `[]`), and the server is left
  running. A `Warm` query never calls `ready`, and neither do diagnostics
  or `after_write`: a server that leaks a token costs one "still loading"
  at start, not a deadline per query, and a warm server re-indexing
  answers from its previous state as it does for any editor.
- `client/lsp/jail.{Placement, Jail, Launch, Executable, ExecutableFile,
  max_link_hops, operation, step_id, locate, regions, policy_for,
  directory_unlinked, caches_unlinked, call_spec, launch, transport}` —
  one server's jail and its transport.
  `Placement.places` (and `manager.Jailed.places`) is the daemon's
  `profile.Places`: it expands `~/` and `<cache>/` roots, and its `home`
  is the server's `HOME`.
  `policy_for` adds each `cache_env` directory to the writable roots on
  both sides and sets its variable last in the environment; `Jail.caches`
  lists them, and `manager.jail_for` makes them (`mkdir -p`) beside the
  scratch's `tmp` before any probe, search or server clears under the
  policy, since bwrap refuses a writable bind whose source is missing.
  `call_spec(jail, op, now_ms:, demand:)` takes the demand from its
  caller: the session's, which the probe proved.
  The transport's relay is a state machine over `Clearing`, `Relaying`,
  `Closing`, `Aborting` and `Draining`; its transition table, and a list
  of the attack each containment rule stops, are in the module doc.
- **Invariant: a server's executable region is directories, never an
  install prefix.** `locate` reads the executable without following it
  (`tools/fs.real_filesystem().read_link`, the existing
  `tools_ffi:read_link/1`; no FFI in this package) and follows a link to
  its regular file, relative text against the link's own directory, at
  most `max_link_hops` (32) links; a loop, an overrun, a dangling link and
  a non-file end are refused by name. `ExecutableFile` is
  `PlainExecutable | LinkedExecutable(chain)`, so `regions` is pure over
  that answer: the executable's directory plus each chain entry's.
  rustup's `~/.cargo/bin/rust-analyzer -> rustup` is `~/.cargo/bin` alone.
  The prefix rule this replaced put `~/.cargo/credentials.toml` in every
  rust-analyzer jail. `unshadowed` still judges every region, a target's
  directory included. `client/codemode.install_prefix` is code mode's and
  is no longer read here.
- **Invariant: no link on the executable's chain is one the server can
  rewrite.** Since the mounts follow where links point, `unrewritable`
  refuses a lease when the command path (if a link) or any intermediate
  link lies at or under the same `writes` list `unshadowed` reads — a
  writable project root, the scratch directory, each `writable` root —
  and tells the operator to name the target in `command`. The final
  regular file is exempt: a plain executable in a writable project mounts
  only its own directory. One check at resolution suffices, because
  nothing re-reads the chain; a link rewritten later points outside what
  was mounted and fails to execute.
- **Invariant: what the helper binds by spelling resolves to where the
  policy said.** `policy_for` stays pure; two disk reads run in
  `manager.jail_for` before anything is made or cleared, both through
  `tools/fs.resolve_real` rooted at `/`. `directory_unlinked(name,
  executable, writes)` refuses an executable whose spelled directory
  lies at or under a path the server writes and does not resolve to that
  write's real path plus the same components (a `node_modules/.bin`
  replaced by a directory link); a link above the write is admitted.
  `caches_unlinked(server, places)` refuses a private cache whose real
  path is not the cache place's real path joined with
  `loom/lsp/<server>/<dir>`, and runs both before `mkdir -p` (so a planted
  link is not followed to make a directory) and after (so the bound
  directory is the one judged). The cache place itself is resolved first,
  so a linked `~/.cache` is admitted. Both are point-in-time: they close a
  planted link, not one swapped between the read and the helper's bind.
- `client/lsp/leases.{Leases, Lease, Refusal, cap_for, start, acquire,
  release, stop}` — the per-session cap on session-lived helper leases,
  `pool_size - reserved_helpers`.
- **Traffic.** `manager.Msg`: `Acquire(identity, reply)` (a caller wants the
  server, started if need be; waiters join one start), `Peek`,
  `Opened(identity, paths)` (a cast recording documents a caller opened),
  `KeeperReady(keeper, outcome)`, `KeeperDown`, `Shutdown(reply:
  Subject(Option(Pid)))` (the reply names the keeper still stopping a
  server). The keeper's own messages are `Begin`, `PreviousGone`, `Release`,
  `ClientDown` and `ManagerDown`: an evicted or released keeper, or one
  whose manager died, stops its client gracefully (`lsp/client.stop`).
- **Serve wiring.** The boot discovers installed extensions once
  (`workspace_plane.prepare`, into `Census.extensions`) and hands both
  readers that answer: the
  `Ready` records' `lsp` go through `profiles.effective_lsp_servers` with
  `Catalog.lsp_servers`, each refusal one `lsp.profile_refused` warning
  (`extension`, `server`, `other`, `reason`), and only `Jailed`
  extensions become tool contributions. `workspace_plane.start_local` builds
  the plane only when the effective server list is non-empty. Each server's `readable`/`writable`
  `~/` and `<cache>/` roots are expanded once with
  `profile.expand_path(_, places)`, where `places` is
  `workspace_policy.home_directory()` and `profile.cache_place` over
  `ffi_os.platform`'s OS name and the daemon's `XDG_CACHE_HOME`; a server
  whose roots will not resolve, or every
  server when the lease counter will not start, is refused with one
  `lsp.unavailable` line and the boot goes on. The counter starts from
  `Settings.helper_pool_size`, `jail.operation` mints the attribution
  operation, and the manager runs as a service-tier child under a minted
  address, over `manager.jailed` with `Settings.demand`. The same door goes
  to `contributions.built_in` and `codemode.over_lsp`, and the wired
  servers' hints go to `contributions.built_in` beside it. `close_instance`
  stops the manager after `api.close` and before the service tree, then
  aborts the plane's operation (the backstop) and stops the counter. A
  daemon session retired through custody loses the manager with the
  service tree; its keeper sees `ManagerDown` and stops the server, and
  pool close is the backstop there.
- `conformance/lsp_e2e_test` is issue #25's acceptance through
  `serve.open_instance`: references, an anchored `fs_edit`, a rename
  preview and apply over a real jailed `gleam lsp`, and a stale apply
  raced by a concurrent writer.

## Code-mode language servers

`codemode.over_lsp(config, Option(lsp/query.Door))` sets `Config.lsp`, the
one field behind four halves of one decision, exactly as `Config.mcp` and
`Config.notes` are: `seam_allowlist` admits `cap/lsp`, the description
therefore renders its surface, `seam_caps_on` appends
`codemode/lsp.serviced_caps`, and `workspace_router` installs
`codemode/lsp.routing` between read-only search and the MCP arm. `cap/lsp`
is on no static allowlist (`codemode/vet/policy.default_cap_modules`),
because its roughly 5.6 KB surface would otherwise sit in every session's
cached prefix to advertise imports that could only be refused; without a
door vetting refuses the import by name and `lsp.*` falls to the default
table's `unsupported_cap`. `lsp_on` excludes extensions and resident hooks.

The field holds the door, not a `codemode/lsp.Seam`, because the seam's
applied rename is bound to one execution's write boundary.
`client/lsp/codemode_rename.seam(door, workspace:, roots:, protected:)`
builds it per request from the request's workspace, its approved writable
roots and its base policy's protected list, the values `workspace_seam`
gives `cap/fs.write`: `door.prepare_rename`, then `tools/lsp.land` with
`fs.write_target` as the target maker, then `door.after_write` per landed
file. A protected path is refused to a rename in `fs_write`'s own words.
`test/client/lsp/plumbing_test` pins both polarities against a fake door.

## Durable code-mode notes

`codemode.serving` installs a narrow `codemode/notes.Door` from Agency's
note and notes callbacks for every selected code-mode surface, including the
explicit WorkspaceOnly deployments. `notes_on` excludes extensions and resident
hooks. The same optional door controls host imports, advertised capabilities,
router wrapping, and quotas. It gives an explicit workspace-only host durable
data access without cap/strand lifecycle authority. The real jailed notes fixture writes
structured analysis, closes SQLite, and consumes it from a fresh program after
reopening through both the typed notes API and the note:// JSON view.

See [protocol 045](../../protocol-change/045-code-mode-notes.md).

## Code-mode utilities

The server defaults to BothSeams; unnamed programs still select workspace and
explicit flags can restrict the offered surfaces. The advertised recipe
fixture runs both exact model-visible programs through the jail, using
scripted child answers and real SQLite notes, then reads orchestration results
from a fresh workspace execution.

## Blackboard read failures

Agency note scans preserve runtime read and decode failures as PlaneFailed.
An empty list means a successful scan found no cells. Child joins propagate
that same refusal when loading saved notes, so a failed scan cannot fabricate
an absent structured result for a completed child. Code-mode get/list and
virtual note reads use this shared path and retain the refusal.


## Caller-owned code-mode message inspection

`client/internal/message_inspection` reads current input membership from coherent
snapshot cuts, rechecks membership when copying pending payloads, and falls back
to the same captured caller leaf for materialized user messages. The router in
`peers` binds the launching strand; program arguments cannot select another
recipient. `peer.inbox` uses exclusive ID cursors and a twelve-item ceiling;
`peer.history` uses exclusive transcript sequence cursors and a sixty-four-item
ceiling. Both inspect without consuming or retaining new records.

`peer_mail` reads existing remote admission receipts through bounded
`snapshot.KeyPage` selections. It sorts captured cells by key before selecting
the scanned window and filtering recipient ownership. Foreign-only pages still
advance. Receipt keys are hashes, so a fresh poll must rescan and reconcile IDs;
the cursor is not an arrival watermark. Exact recipient lookups enforce target
identity, and sender lookups require a current outgoing link and bind the source
session and strand. Admission proves neither consumption nor a model read.

Protocol 056 records the public capability expansion and checkpoint policy.
The production next-request regression proves that queued local and remote
bodies appear in the first request after the current whole tool batch completes.

## Explicit session-lifetime background jobs

Protocol-change/058 records the lifetime contract. Finite jobs retain their
existing default and fixed deadline. Bash `mode: "background", lifetime:
"session"` and `cap/job.start_for_session` explicitly request no wall deadline.
Code-mode callers declare `permissions.wall_s: 0`; the launching action must
receive the missing wall grant before execution, and the job captures it.
Zero job wall/deadline denotes this authorized lifetime. Clearance and drain
remain bounded, other resource limits remain active, and session shutdown,
owner kill cancels the execution. A session job clears under a separate
custody operation; aborting the originating model turn leaves it running.
Finite jobs keep originating-operation cancellation. The durable `started_by`
field retains the launch's operation for audit. Quiet waiting
has no completion or heartbeat wake unless the caller explicitly asks for the
existing idle heartbeat. A VM restart loses the job and never replays it.

## Typed schedule projections (protocol 057)

`client/scheduleseam` projects its existing `client/schedule.Timing` into
`tools/schedule.Cadence` and `Expiry` for create and list responses. The
`client/codemode` adapter translates these into the workspace router's matching
types. Relative one-shots expose their resolved Unix second, recurring timings
expose granted bounds, and cron preserves its fixed UTC offset. Neither the
durable schedule encoding nor wake authority changes.

See [protocol 057](../../protocol-change/057-typed-capability-results.md).

## Standalone MCP dependency

`gleam_mcp` owns MCP protocol codecs, JSON values, framing, the client state
machine and native transport. `client/mcp` sets `with_client_name("loom")`
before connecting and preserves the parked-client publication and retirement
contract. The local `mcp` package retains code generation, name/schema
planning and direct `core/msgpack` conversion. `gleam_mcp/protocol.Other`
retains a raw non-text block; `client/mcp.content_block` deliberately drops
that payload when constructing the existing capability result. Core JSON
and capability wire contracts are unchanged.

## Watcher consent and LSP reliability (protocol 061)

`permissions.remembering_action` prepares the reserved
`client/action_grants/<digest>` fact for a singleton wall-zero grant. Its
identity includes the requesting strand, tool, and complete argument digest.
The gateway commits it atomically with the captured approval.
`permissions.read_for` adds it only to that matching invocation; general
`permission_grants` never acquire an unlimited wall grant. Mixed requests
and other resource grants remain once-only. Running jobs retain the authority
captured at launch, and session close still joins their cancellation.

## Remembered permissions: provenance and forgetting (protocol 073)

`client/permissions` also records who approved each remembered grant and lists
and forgets what a session remembers. The general fact keeps `grants`, the only
thing dispatch reads, and adds `version: 2` and a `remembered` array of
`{grant, provenance}` rows. A `Provenance` is `Approved(by, via, at_ms)` or
`Unknown`: `by` is the principal of the approving connection's binding, `via`
is `Login`, `Device` or `Uncredentialed` with the credential digest's
fingerprint, and `at_ms` is the gateway clock at commit
(`gateway.approval_provenance`). A web page's connection carries the fingerprint
of its browser sign-in in `Binding.signin`, copied from the page's own record,
and that is recorded as `Login`; any other connection records the kind of its
credential digest (`access.credential_kind`). A `loom ui` page is admitted under
the terminal's bearer, so the digest alone would label its grants `Device`. The decoder is total
and advisory: a missing or damaged row reads as `Unknown` and never changes what
is permitted, and a fact with no `version` reads with every grant `Unknown`.
Exact-action consent cells gain `tool`, `strand`, a preview of at most 200
characters and the same provenance.

`permissions.listing` reads the general fact and every consent cell with their
sequences, and `permissions.board` encodes it as the `permissions` snapshot
(`protocol.PermissionsSnapshot`). `permissions.forgetting` builds the guarded
edit for a `protocol.ForgetTarget` (one grant, one consent by its lower-case
hexadecimal id, or everything) as the operator saw it, and the gateway applies
it through `api.edit_reserved_facts`: a moved cell, or a target already gone,
is `Stale` and answers `conflict` with nothing written. The general fact is
rewritten and never deleted. `PermissionsGet` and `PermissionForget` count as
mutations in `gateway.read_only`, so an observer attachment is refused both and
neither runs while the session drains. Both, and `ApproveForSession`, are also
owner-only (`gateway.owner_only`): an authenticated attachment whose principal is
a member is refused with `forbidden`.

`lsp/jail.workspace_reads` intersects the workspace with session-authorized
readable and writable roots. Sibling dependencies become readable while writes
remain scoped to the selected package and private scratch/cache. Network-off,
protected paths, executable-region checks, and answer admission are unchanged.
The protocol client now preserves bounded error-level server window messages.
After a failure, empty answers and clean-looking diagnostics become
`Unavailable` until a substantive semantic answer demonstrates recovery.

The setup guide distinguishes host extension commands from a jailed Bash
command's private HOME. Neither the private installation nor a nested probe's
scratch refusal proves the daemon's global profile is missing or broken.

## Finite language-server observations

`client/lsp/manager.observation_door` fills `lsp/observation.Door` beside the
interactive `query.Door`. Collection admits one configured server and root,
pulls the explicit source documents into that server, waits for readiness,
and queries only the requested outlines and reference seeds. It uses one
weft worker under the lesser of the invocation deadline and 75 seconds.
The death of its caller cancels that worker and its pending protocol id;
collection never stops the shared language-server lease.

A complete batch is bounded by 10,000 retained facts, 4 MiB of retained text
and fact strings, and 128 semantic requests, including seed outline
resolution. Counts retain server-withheld locations without reading or
opening them. A failure, unsupported feature, busy server, exceeded bound or
detected change refuses the whole batch. Canonical paths, content digests,
internal document versions, the opaque client generation and the checked
start/end interval describe the admitted observation.

The interval checks synced source text and actor state before and after the
queries, then rereads every admitted fact document and checks the original
source spellings. Returned reference files can carry no actor version when
the server never held them open. Those files are first read after the server
answer. Standard LSP has no transactional project revision, so the batch
does not establish atomic workspace coverage or detect edits in unseen
dependencies. SQL execution belongs to the code-mode satellite.


## Approved LSP dependency preparation

`lsp/profile.Preparation` is approved profile data. `AlreadyPrepared` is the
backward-compatible default; `GleamDependencies` authorizes the fixed, finite
`gleam deps download` recipe. `lsp/preparation` creates a separate broker
`CallSpec` with full network, a 60-second CPU/wall bound and bounded output.
`lsp/manager.connect_jailed` runs it after the enforcement probe and verifies
metadata before constructing the offline transport. `lsp/jail` gives setup and
server the same private HOME and XDG cache for cross-platform Gleam cache lookup.

`lsp/dependency_state.fingerprint` gates workspace-local real paths against
protected entries and the session’s existing readable or writable roots,
follows at most 64 package configurations, and retains
128 KiB at most per metadata file. The generated package inventory hashes
canonical table/string values, preserving package membership, versions and git
commits while ignoring the key order Gleam rewrites from maps. Unsupported
inventory values or more than three table levels refuse reuse. Manifest and
project configuration changes still use their complete source digests.
The query caller sends only a digest in
`manager.Acquire`; the actor compares it with the successful keeper start's
`Started.stamp`. A change uses the existing eviction path, reruns setup, and
reopens held documents. This snapshot tracks dependency preparation inputs,
not transactional workspace coverage. Profiles without the recipe skip these
reads and retain their existing network authority. Protocol 064 records the
new consent boundary and the refused external path-dependency scope.

## Model profiles (protocol 076)

`[profiles.<name>.roles]` tables name the roles a profile replaces
([protocol 076](../../protocol-change/076-config-profiles.md)).
`catalog.parse` validates every profile when the file loads, with the same
`parse_role_table` as `[roles]` and the table's own path in each message, and
`Catalog.profiles` holds each one already merged over the default role set.
`catalog.select_profile` returns the catalogue with `roles` replaced, or
`unknown_profile`'s message naming the profiles that exist.

The profile is part of the registration (`catalogue.Registration.profile`, a
name and never roles) and of `manager.Creation`. `serve.resolve_managed` passes
it to `resolve`, whose `load_config` applies `select_profile` before
`catalog.gateway` builds the session's gateway, so every consumer of roles reads
its own session's `Settings.gateway` or `Settings.catalog` and nothing is cached
daemon-wide. Every open resolves it again: a profile the file no longer defines
fails the open with the file's message, which the registration's startup reason
(055) carries, and never opens on the default roles. A profile with no config
file fails the same way. `serve.build_domain` passes no profile, so the shared
workspace domain's maintenance uses the default roles.

`server.create_session` checks the profile before reserving an identity
(`daemon/profiles.check` against the request's configuration, or the daemon's
own), and answers the code `unknown_profile`. The control dispatch words it with
the profiles that exist, the one refusal whose message is not "request refused";
the owner check runs first. `sessions.create` decodes `profile` as optional and
refuses a present value that is not a profile name
(`storage/catalogue.is_profile_name`), never reading it as the default.
`HomeAttachment.profiles` reads the names for the web form once, at page open,
and only for a page that holds the creation capability; `ui_socket.create_for`
takes the chosen profile and maps `unknown_profile` to
`creations.UnknownProfile`.

## Request accounting

Provider settlement and failure carry `core/accounting.RequestAccounting`.
The relay preserves that report through terminal delivery and uses an empty
report for refusals before dispatch. Goal accounting adds aggregate usage and
folds `cost_evidence` alongside the durable cursor; uncached token goals count
`input + output`, since input already excludes cache buckets. Client usage
pushes carry aggregate consumption and optional `last_usage` separately.
Context readers suppress only unknown all-zero final observations, retaining
historical nonzero readings and reported zero measurements.

`distill.Answer` keeps the final attempt's usage apart from request accounting;
`distill.Failure` retains the same report without an answer. Extraction and
consolidation persist failed request consumption in the memory ledger.
Glance and block summary requests commit one aggregate row with phase details
before parsing or publishing the text. A ledger commit failure prevents that
publication; an empty report writes no row. These auxiliary rows do not
replace the primary operation's context measurement.

### Pinned main model (protocol 080)

A session can also be created on one `[models.<key>]` entry. The key is a second
optional `sessions.create` field (`protocol.CreateSession.model`, decoded by
`model_field` against `storage/catalogue.is_model_key` and refused when present
and not a key), a field of `manager.Creation` and of the registration
(`Registration.model`, compared on a retry like the profile), and an argument of
`serve.load_config`, which applies `catalog.select_model` after
`select_profile`: the `main` chain becomes that entry alone and no other role
moves. Every open resolves it again, so a key the file no longer defines fails
the open with `unknown model "x"; the configuration defines: ...` and never
opens on the default model; with no config file it fails like a profile does.
`daemon/profiles` answers the daemon's pre-reservation question for both:
`check_choice` judges the profile and then the model (`check_model`, a third
`Refusal`, `UnknownModel`), `server.create_session` maps it to the code
`unknown_model`, and the control dispatch words it with the keys that exist.
`HomeAttachment.models` reads `profiles.model_keys` for the web form once at page
open and, as for profiles, only for a page that holds the creation capability.
`ui_socket.create_for` and `create_task` take a `creations.Roles` in place of a
bare profile and map `unknown_model` to `creations.UnknownModel`. The observer
socket's admission is untouched: the form's existing submit carries the field.

The model composes with a session's place (protocol 078): `manager.Creation` and
the registration carry the model beside `executor` and `pool`, `reserve_creation`
compares all of them on a retry (a pooled request still does not compare the
executor), and a session that moves keeps it because `session_move.Manifest`
carries `model` and `session_importer` registers it on the receiver. The web
form for a registered workspace draws no model select, so `create_for` passes
`creations.default_roles` for that place; the control command can pin a model on
an executor or pool session.

## Trusted distribution membership (protocol 078)

`client/distribution` is the membership layer for orchestrator and executor
daemons that trust each other as Erlang nodes. It starts TLS distribution on a
VM that was booted for it and connects to the configured peers. It sends no
message, registers no name and runs no service; the executor role and remote
tool calls are later slices that use the `Peer` it returns.

`Config` is built by `configure` (names, absolute credential paths, one to 32
distinct peers each with a 32-byte pin, optional `listen_port`) or read from
the `[distribution]` table by `from_document` / `parse`; both return a sentence
naming the key, and `None` when the table is absent, which means distribution
stays off. `catalog.parse` validates the same table, so a typo is refused
wherever the file is read. `tls_options` renders the `ssl_dist_optfile` text
and `boot_arguments` the three flags. `start` returns a `Membership`; `peer`
resolves a configured name to a `Peer`, `connect` makes an explicit hidden
connection under a weft deadline, and `describe` words a `Fault` for the
operator.

`client/internal/ffi_distribution` and `src/client_distribution_ffi.erl` are
the only foreign code: the ssl `verify_fun`, `net_kernel:start/2` and
`hidden_connect_node/1`, the checks of the emulator's own boot arguments, and
the launch of `epmd`. None of it is expressible in `gleam_erlang`, `gleam_otp` or `weft`. The verify
callback keeps every PKIX failure, and for the leaf it requires the SHA-256 pin
and the exact node name (the certificate's only DNS name containing an `@`) of
one configured peer, in both directions. Network input never makes an atom:
peer names become atoms once inside `start`, and `peer` looks them up by string.
The foreign code also has a live mode (protocol-change/086), which `[distribution]
ledger` selects: `tls_options` renders no pin and the callback checks a leaf
against the `pins` row of the protected ETS table `loom_dist_pins`, refusing
every handshake with `ledger_unavailable` when the table is absent. See
"The membership ledger, TLS and dialing (slice 1c)" below.

Invariants that break things when violated:

- `start` checks the boot before it reads a credential: not already distributed,
  OTP 29 or newer, `-proto_dist inet_tls`, `-ssl_dist_optfile` present, no
  `-name`, `-sname`, `-setcookie`, `-nocookie` or `-ssl_dist_opt`, and an options
  file that is private and equals `tls_options` for this configuration. Each
  failure is its own `BootRefusal`, so the operator is told what to change.
- The configured cookie path must be the emulator's `$HOME/.erlang.cookie`,
  since the emulator reads the cookie from the init home when it opens its
  listener. The key and cookie must be mode 0600.
- `dist_auto_connect` is `never` and the node is hidden. Do not add a code path
  that dials a node that is not a configured peer, and never connect from
  inside a satellite: satellites boot with `-proto_dist none` and their
  environment is built from an allowlist, so `ERL_FLAGS` is not inherited.
- A failed `start` stops the partial distribution (`net_kernel:stop/0`) and
  returns no `Membership`.
- An epmd must answer before `net_kernel:start/2`. The daemon VM is booted
  without `-name`, and unlike `erl -name` at boot, a dynamic `net_kernel:start/2`
  never launches epmd, so on a machine with no epmd running the node fails to
  register (`econnrefused`, then `nodistribution`). `ensure_epmd` in the FFI asks
  loopback (under a deadline, since `erl_epmd:names/1` has none) and, when
  nothing answers, runs `epmd -daemon` from `code:root_dir()/erts-<version>/bin`
  or `PATH`, then polls up to three seconds. It does nothing under
  `-start_epmd false`, which the tunnelled-orchestrator recipe sets. The port is
  the VM's `epmd_port` argument (from `ERL_EPMD_PORT`); `ERL_EPMD_ADDRESS`
  reaches the daemon through the inherited environment. Failures are
  `EpmdUnavailable(port)` and `StartFailed`, never `InvalidCredentials`: that
  fault now means only a credential read or match failed. The fixture's
  `raw_wait` calls `ensure_epmd` too, so it works with no epmd.

The daemon calls `start` from `daemon/main.prepare_startup` before the
catalogue is opened and refuses to start with `describe`'s sentence, so a VM
booted without the flags fails before any state is touched. The launcher
`bin/loomd` (Makefile `server-shipment`) appends `-proto_dist inet_tls
-ssl_dist_optfile "$LOOM_DISTRIBUTION_OPTFILE"` to `ERL_FLAGS` when that
variable is set and is otherwise unchanged. `daemon/distribution_cli` is
`loomd distribution options CONFIG OUTPUT`, which writes the options file with
mode 0600 from the same table.

`test/client/distribution_test.gleam` covers configuration refusals in process
and drives `test/client_distribution_fixture_ffi.erl`, which mints certificates
and boots separate emulators with the production flags: a correct pair connects
and exchanges a message; a wrong leaf pin (either side), a pin that matches but
carries another node name, a pin that matches but was issued by an untrusted
CA, and a cookie mismatch are each refused; a send to an unconnected peer does
not connect; and the boot and credential refusals leave the VM non-distributed.
Four scenarios give the child a private `ERL_EPMD_PORT` and so hold with or
without a machine epmd: start launches an epmd that registers the node and
stops it afterwards, a port held by a listener that never answers gives
`EpmdUnavailable` without hanging, a taken `listen_port` gives `StartFailed`,
and `-start_epmd false` launches nothing.
Removing the pin comparison, the name comparison, the PKIX failure or
`dist_auto_connect = never` from the callback fails exactly its own scenario.

`test/client/daemon_shipped_remote_test.gleam` is the shipped-daemon end to end
(issue #697, gated on `LOOM_BOOTSTRAP_E2E_SERVER` like the other shipped
fixtures, and on the sibling `bin/loom-exec` enforcing a policy, which
`enforcement.probe` measures; `make server-shipment` alone does not build the
helper, `make binaries` or `make sandbox` plus an install into `bin/` does).
`test/support/remote_daemons.gleam` mints a CA and per-node leaves with
`openssl`, writes each node's cookie at that node's isolated `$HOME`, renders the
`loom.toml` files and `loomd distribution options`, and starts an orchestrator, an
executor and a second orchestrator with a decoy pin through a wrapper that sets
`LOOM_DISTRIBUTION_OPTFILE`. `test/support/remote_probe.gleam` runs as a throwaway
emulator that each daemon lists as a peer and reports connections, makes a daemon
dial or drop a connection (`erpc`), and lists a daemon's hidden nodes. The
orchestrator's model is a `provider_http` script on loopback. Three tests share
the fixture:

- `daemon_shipped_remote_test_`: wrong pins are refused in each direction and no
  daemon dials the executor before a session exists; `sessions.create` with
  `executor`/`workspace` settles `resident`; the model writes a file with
  `fs_write`, reads it with `bash` and `fs_read`; the file is under the
  executor's checkout and nothing named like it or like `repo` is on the
  orchestrator; the executor then lists the orchestrator as connected with no
  probe dial; a stop leaves the executor's scope `closed`/`all_retired` at
  incarnation 1 (read from a copy of `exec-ledger.db`), and a reopen attaches at
  incarnation 2 and reads the earlier file.
- `daemon_shipped_remote_restart_test_`: the orchestrator is killed with
  `SIGKILL` while a `bash` call sleeps on the executor and started again. The
  reopen waits out the dead daemon's writer lease (up to 60 s), recovers the call
  from the executor's ledger, and the model gets the stored outcome; the
  side-effect file shows one run.
- `daemon_shipped_remote_partition_test_`: the probe has the orchestrator
  `disconnect_node` the executor mid-call; the surface repairs the link and
  re-sends the same `Run`, the file shows one run, and the executor lists the
  orchestrator again afterwards.

`test/client/daemon_shipped_remote_codemode_test.gleam` runs the two heavier
effects on the same two-daemon fixture, with no probe node. Both tests need the
build seed (`make codemode-seed`), which the fixture hands the executor with
`remote_daemons.start_with(layout, ["--codemode-seed", seed])`; the executor
reads that flag as a local daemon does (`executor_plane.machine`). The fixture
directory is a short one under `/var/tmp` because code mode's capability socket
is bound below the executor's state root and an AF_UNIX path is about a hundred
bytes. A missing seed, `gleam` or `rg` prints a `SKIP shipped remote code mode`
line and passes. The provider is started with `provider.with_server_for` for a
longer callback budget and, in the code-mode test, `provider.AlsoFailed`, which
lets the model receive an error result.

- `daemon_shipped_remote_codemode_test_`: one `code_mode` program is vetted,
  compiled and launched on the executor, writes a file through `cap/fs` into the
  executor's checkout and reports a computed value with the text of `README.md`;
  a second program that does not type check comes back as an error result with
  the compiler's diagnostic. The program's file and the `.codemode` and `.blobs`
  directories are absent from the orchestrator, and a stop leaves the scope
  `closed`/`all_retired`.
- `daemon_shipped_remote_lsp_test_`: the executor's `loom.toml` carries
  `[lsp.gleam]` and the orchestrator's does not. A `cap/lsp` program reads the
  hover, definition and references of `greet`; a rename preview names three files
  and leaves the disk alone; the apply rewrites all three in the executor's
  checkout with settled, empty diagnostics; an `fs_write` of a module that does
  not type check returns a settled diagnostics block with one error, and
  `lsp.diagnostics` reads it back. The executor is started with `--helper` naming
  the real `bin/loom-exec`, not the isolated launcher's symbolic link to it: the
  helper re-executes itself inside the sandbox profile, a language server's
  profile reads only the system directories and the project, and the link lives
  in the fixture's home, so the profile cannot follow it (the exec fails with
  `sandbox-exec: execvp() ... Operation not permitted` and the probe exits 71).
  `bash` and code mode run under a profile that reads the whole host and never
  notice.

`test/client/daemon_shipped_remote_tools_test.gleam` covers the rest of the
phase 1 workspace plane on the same fixture, one pair per test, with the pair's
arrangement in `test/support/remote_pair.gleam` (gating, the short `/var/tmp`
directory, credentials, both configuration files, `open_registered`,
`session_fact` and `session_socket`). `session_fact` reads a reserved fact from
a copy of the orchestrator's session store, and `session_socket` is the owner's
socket on the session for commands such as `goal_set` and `worktree_diff`. A
host without `rg` or `git` prints a `SKIP shipped remote tools` line and passes.
`provider_http` admits the advisor's standing brief (`advisor.brief`) as a
leading block of the first user message, so a scripted reviewer can be fed.

- `daemon_shipped_remote_search_and_cwd_test_`: `grep` finds a pattern in files
  only the executor holds and honours its `globs`; `working_directory` selects a
  subdirectory, the next `bash` reports the executor's path, a `bash` call's own
  `cwd` leaves the selection alone, the orchestrator's store holds
  `client/working_directory/main`, and after a stop and reopen a `bash` call
  still starts there.
- `daemon_shipped_remote_jobs_test_`: a `bash` call whose `timeout_ms` passes
  becomes a background job, and ends when the test creates a file in the
  checkout; the executor's completion notice wakes the idle session through the
  owner port, and `job_poll` reads its exit status and output. A second job,
  started with `mode: "background"`, appends to a file every fifth of a second;
  `job_kill` stops it and the file stops growing. `live_jobs` lists both while
  they run and none after, and the store holds a `job/<id>` record for each, in
  the phase it ended in.
- `daemon_shipped_remote_hooks_test_`: the checkout's `.claude/settings.json`
  runs nothing until the orchestrator holds a trust record for the hash of the
  bytes the executor sent (the test writes it with `hooktrust.trust`, as the
  absent `loom hooks trust` will). Then a `SessionStart` hook runs once and its
  output rides the first request, a `PreToolUse` hook runs on the executor
  around a `bash` call with the call as stdin and the checkout as its working
  directory, a `PostToolUse` hook replaces the output the model reads, and a
  `PreToolUse` hook that exits 2 refuses an `fs_write` with its message as the
  error result. A `PostToolUse` note added beside the output is a second content
  block, which `provider_http` does not admit, so the hook rewrites instead.
- `daemon_shipped_remote_goal_test_`: a goal's check runs in the executor's
  checkout. One check passes on a file only the executor holds and prints the
  executor's path; the other fails because the file it names exists only in the
  orchestrator's launch directory. The board and the reviewer's feed (a second
  scripted provider on the `advisor` role) carry each result.
- `daemon_shipped_remote_git_and_guidance_test_`: the executor's `AGENTS.md` and
  `CLAUDE.md` and the operator's global `AGENTS.md` reach every request's system
  prompt, and same-named decoys in the orchestrator's launch directory, under the
  registered name there and in its home reach none. A `git` call reports the
  executor's branch and commit; the orchestrator's store holds that commit as
  `session/git-start` and the root and branch in `client/peers/git-observation`;
  the `worktree_diff` board lists a file the model added. The system prompt has
  no Git state (`prompt/default`), local or remote.

`test/client/daemon_shipped_remote_strand_test.gleam` and
`daemon_shipped_remote_owner_loss_test.gleam` run a `code_mode` program on the
executor that spawns a child strand, on the same `remote_pair` fixture. Both
need the build seed (`remote_pair.code_mode_seed`, passed to the executor as
`--codemode-seed`) and print a `SKIP shipped remote ...` line without it. A
spawned child makes the glance loop ask the `summarize` role for a title, and an
unrouted role falls back to `main`, which is the scripted provider and refuses
the request as out of order; `remote_pair.models_without_glance` routes the role
to a closed port so the script lists only the conversation under test. A child's
first request is matched by `AwaitPromptPrefix(strand_framing.brief_head("main"))`.

- `daemon_shipped_remote_strand_test_`: the program calls `strand.spawn` and
  `strand.wait`; the orchestrator's Agency creates the child and runs its turn
  against the same scripted provider. The child's `bash` call prints the
  executor's checkout as its working directory and writes a file there, which is
  absent from the orchestrator. The child's final text returns through the join
  into the program's report, and the scope closes `all_retired`.
- `daemon_shipped_remote_owner_loss_test_`: the program joins a detached child
  whose `bash` call takes 40 seconds. After the call has started, a probe (the
  third distribution node, issued by `remote_pair.probe_identity`, listed in
  both configurations by `remote_pair.configure_trusting`) has the orchestrator
  drop its connection to the executor (`remote_pair.drop_link`). The program is
  told `owner_unavailable` and the model answers within 20 seconds of the cut,
  with the child's command run once and the provider's script consumed in order.
  An owner that is up and silent is not covered: its bound is a constant of the
  executor, and no daemon can be made to hold a reply back without a hook in
  production code.

### Provisioning a deployment (issue #697)

`loom distribution` (also `loomd distribution`, and `dist` for either) takes an
operator from nothing to a trusted deployment with no openssl and no script.
`loom` forwards the verb to `loomd` the way it forwards `ext`
(`packages/tui/CLAUDE.md`); the implementation is here, in five modules that
each own one step:

- `client/distribution_plan` reads the plan (`[[node]]` tables, in TOML or JSON
  by file extension) into a neutral `Tree`, decodes it once, and validates it:
  unique executor-grammar node names and Erlang names, roles closed, `executors`
  only on orchestrators and naming executors, `workspaces` only on executors
  under `catalogue.is_workspace_name` with absolute roots, every executor used
  and every orchestrator peered. `peers` derives the edges: an orchestrator
  peers with the executors it uses and with the other orchestrators, an executor
  with the orchestrators that use it, always symmetrically.
- `client/distribution_provision` mints one authority, one certificate per node
  and one shared cookie, builds a `Bundle` per node and the secret-free
  `System` (`system.json`), and writes `<node>.loombundle` (mode 0600) and
  `system.json` into a 0700 directory it refuses to reuse without `--force`. The
  CA key is dropped when `provision` returns. `render` is `show`.
  `provision_ledger` is the ledger mode (below), which the command uses unless
  `--static`: it keeps the key and the cookie, returns them as an `Authority` for
  the caller to seal or drop, and `write` adds `ledger.log`.
- `client/distribution_bundle` is the single-file bundle: JSON, marker
  `loom-distribution-bundle/1` (or `/2`, below), the node's role, `Trust` (peers
  with pins, or a ledger), role tables and the CA, certificate, key and cookie
  inline. `decode` is the install gate:
  strict keys, the plan's own `check_node`, the daemon's own `configure` for the
  node and peers, the cookie rule, and `ffi_pki.inspect` (readable PEM, chain to
  the CA and validity, key matches certificate, exactly the node name as the
  only `@` DNS name, the host present). `config_tables` renders the `loom.toml`
  tables the bundle owns.
- `client/distribution_install` is `install BUNDLE [--home] [--config]
  [--force]`. It decides every destination (create, unchanged, replace or
  refuse) before it writes anything, merges the owned tables into `loom.toml` at
  the text level (append what is missing, skip what is equal, refuse or with
  `--force` remove the bundle's own sections and append), reads the merged text
  back through `distribution.from_document` and `executors.from_document`, and
  generates the options file from the merged `[distribution]`. A different
  existing cookie at `$HOME/.erlang.cookie` is refused unless `--force`, since
  Erlang reads one cookie for every node the user starts. Rerunning with the
  same bundle changes nothing.
- `daemon/distribution_cli` dispatches `init`, `provision`, `show`, `install`,
  `options` and the five ledger commands (`daemon/ledger_cli`, below). `run`
  returns the text to print; `main` prints and halts. No command prints a key,
  cookie, passphrase or certificate. `provision` writes a ledger unless given
  `--static`, and takes `--drop-authority`, `--passphrase-file` and `--home`
  (below); the tui carries a byte-identical copy of the usage text, which
  `scripts/cli_help_test.sh` compares.
- `daemon/executor_cli` is `loomd executor release SESSION [--state-dir PATH]`
  (`loom executor release` forwards to it from the launcher, with the help text
  duplicated in `tui` and compared by `scripts/cli_help_test.sh`). It takes the
  state directory's endpoint reservation through `daemon.claim_endpoint`, which
  refuses while a live daemon holds it, then opens `<state-dir>/exec-ledger.db`
  and calls `exec_ledger.release`. It never creates the state directory. The
  orchestrator's side of the same exit is `scope.attach_at`: a close with unknown
  cleanup attaches at the next incarnation, which the executor refuses until the
  release and reopens after it.

The only new foreign code is `src/client_pki_ffi.erl` behind
`client/internal/ffi_pki`: `authority`, `issue`, `pin`, `inspect` and
`random_bytes` over OTP `public_key` and `crypto`, because no Gleam library
generates or signs X.509, then `sign` and `verify` for the ledger, and
`derive_key`, `encrypt`, `decrypt` and `read_secret` for the authority key at
rest (PBKDF2 and AES-GCM, which `gleam_crypto` lacks, and a prompt that does
not echo). Keys are ECDSA P-256, signatures
ecdsa-with-SHA256, the authority lasts 3650 days and node certificates 1825 days.
Renewal, adding a node without reissuing the others, and revocation are certificate
rotation, which is later work: today a changed deployment is provisioned again and
every bundle is reinstalled with `--force`.

Invariants:

- A bundle holds the key and the cookie. It is written 0600 through
  `bootstrap.atomic_write_private`, and no command or error message may contain
  `Bundle.key`, `Bundle.cookie` or any PEM. The tests assert this for the
  summary, `system.json`, `show` and `install` output.
- `install` writes nothing when any destination is refused. A new destination,
  or a new refusal, goes into the plan phase before `ensure_private_directory`.
- The text merge touches only `[distribution]` (with its peer rows),
  `[executors.<name>]` and `[workspaces.<name>]` sections the bundle names. The
  read-back in `accepted` is what proves a section was replaced; keep it.
- A plan or bundle rule is the daemon's rule by construction (`check_node_name`,
  `configure`, `is_executor_name`, `is_workspace_name`), never a copy.

`test/client/distribution_provision_test.gleam` covers the plan in both
spellings, each refusal, provisioning modes and `system.json`, the pins and the
exact certificate names (recomputed in `client_distribution_fixture_ffi` with
`public_key` directly), install into scratch homes, idempotence, the cookie,
conflicting-table and credential refusals, tampered bundles, the commands, the
`dist` alias, and two real emulators booted from files `install` wrote: they
connect with pins checked on the executor's fixed port, and changing one peer pin
in the orchestrator's installed `loom.toml` makes the connection refuse.

## Naming a registered workspace (protocol 078)

`sessions.create` takes an optional `executor`. `client/executors` decodes the
`[executors.<name>]` tables (`node` only, which must be one of the
`[[distribution.peers]]` nodes, so a document with executors and no
`[distribution]` is refused); `catalog.parse` validates it with every other table
and `daemon/main.prepare_startup` captures the list once in `Config.executors`,
which `listen_serving` copies into `server.Config.executors`. It is never reread.
A new `[executors.<name>]` key means `executors.row`, `scripts/config_keys.sh`
and `docs/configuration.md`.

With an `executor` the workspace is a registered name. `protocol` decodes it
under `catalogue.is_workspace_name` (no `/`, no NUL, at most 128 bytes), takes the
executor under `is_executor_name`, defaults the domain scope to `session_only` and
refuses `workspace_private` as malformed. `server.create_session` then skips
`bootstrap.canonical_directory` for it, answers `executor_unknown` when
`executors.find` misses, does not call `manager.remember_folder`, and passes the
name through `manager.Creation.executor`, which `reserve_creation` compares on a
retry. `view_json` and the owner `sessions.get` add an `executor` member only
when it is non-empty, so a local record is byte-identical.

Every use of `Registration.workspace` that a registered session reaches is
guarded by the shape of the text (a name never starts with `/`) or by the
executor itself: `serve.resolve_managed` resolves a registration with
`RegisteredWorkspace`, so `resolve` neither canonicalizes the name nor looks for a
helper or Go caches for it;
`ui_project.locate` returns `None` for a workspace that is not an absolute path;
`ui_socket.known_workspace` only matches local sessions, so a home page can never
re-create into a name; `catalogue.set_workspace_default` refuses a registered
session; the domain record holds the name under a session-only domain, which
`storage/domain.validate` alone allows. Creating such a session starts its
opening (create initializes) and the daemon assembles it with
`serve.assemble_registered` (see "Assembling a registered session" below). A
daemon that cannot reach the executor (no `[distribution]`, an unconfigured name,
a refused handshake or attach) fails the build, and `operations.get` reports
`start_failed` with an `executor_unavailable: ...` reason; the registration stays
`reserved`, so `sessions.open` answers `not_initialized` and the creation key
retries. `test/client/daemon_registered_test.gleam` covers the wire, and
`executors_test` the table.

Both first-party clients create such a session. The terminal sends the same
`sessions.create` from `loom --executor <name> --workspace <registered name>` (see
`packages/tui`). The web home reaches `create_session` through
`ui_socket.create_for` with a `creations.Registered(executor, workspace)` place:
`placed` judges only the shape of the name (`creations.registered_name`) and never
calls the folder check, `executor_of` puts the executor on the `manager.Creation`,
and `scope_of` makes a registered place session-only whatever the form said, which
the control decoder does for `domain_scope` and a direct `create_session` call does
not. `server.create_session` then answers `executor_unknown` for an executor the
daemon does not configure, which `creation_refusal` words as
`creations.UnknownExecutor`. The page learns the executor names from
`server.HomeAttachment.executors` (`list.map(config.executors, ...)` in
`home_upgrade`), which `ui_socket.upgrade_home` hands to `home.Start.executors`
only for a page that holds the creation capability; nothing about it is on the
control protocol. `listed_entry` copies `Registration.executor` into the sidebar's
`sessions.Entry`, and `with_projects` skips a registered entry, whose workspace is
a name and not a path to look a repository up from.

## Remote tool calls (protocol 078)

`client/remote/*` is the mechanics of running a session's tool calls on another
machine: the executor's host, the orchestrator's surface and owner port, and the
closed message vocabulary between them
(`docs/design-notes/distributed-runtime.md` section 6, `protocol-change/078`).
It is generic over the workspace plane, so nothing here depends on how `serve`
assembles one; an integration supplies a `host.PlaneFactory` on the executor and
composes `surface.Functions` into a `ToolSurface` on the orchestrator. The
orchestrator side is assembled by `remote/workspace` and `serve`, and the
executor side by `daemon/main.start_executor`, which starts the host from
`[workspaces.<name>]` rows with `executor_plane.factory`; both have sections
below.

- `remote/protocol` is plain data only: `Key`, `HostMessage(census)` (`Attach`,
  `Run`, `Query`, `QueryOrFence`, `ListUnacked`, `Ack`, `Close`), `OwnerMessage`
  (one constructor per `OwnerServices` function, plus `Tail` as a cast), the
  replies (`RunAnswer` = `RunFinished | RunLost | RunRefused`, `Lookup` =
  `Missing | Admitted | Terminal | Unknown | Fenced`), `Refusal`, and
  `CloseOutcome`. The census is a type parameter the host never reads.
  `unknown_outcome_text` is the model's wording for a lost call and
  `did_not_run_text` the wording of a fenced one. `Attach` carries
  `protocol.version` (3); the host refuses any other value with `VersionMismatch`.
  Change the version whenever a constructor or field changes. Version 2 moved
  the executor's clock reading out of the census and into `Attached`, so each
  reply carries the time it was sent. Version 3 added background executions
  (`StartExecution`, `StopExecution`, `LaunchExecution`, `InteractExecution`,
  `Unacked.executions`, `Lookup.Executed`) and `AskMcpPlan`, the MCP plan the
  executor asks the owner port for while it builds a plane. `Attach` keeps the
  shape it had in version 2, so a peer of either version reaches the version
  check and is refused by value; never add a field to it, or the refusal
  becomes a crash of the host and the executor daemon. Every other message
  assumes a peer built from the same source.
- `remote/address` is `{registered name, node}`. `deliver` and `watch` go through
  `internal/ffi_remote`, three stock-OTP `@external`s (`erlang:send/2` and
  `erlang:monitor/2` on a `{Name, Node}` destination, and `gleam_stdlib`'s
  identity to make a `Name` from fixed text) because `gleam_erlang` resolves a
  named subject only on the sending node. No `.erl` file, and no atom is made
  from network input.
- `remote/codec` stores a `ToolOutcome` as the `core/codec` JSON in an envelope
  and decodes it totally.
- `remote/host` is the executor's node-level actor (`weft/actor`), registered under
  `address.default()`. It owns the one `storage/exec_ledger` handle, builds a
  `Plane(census)` per session through the `PlaneFactory` as a weft run (the
  scope is `Building` until it reports), runs each tool as a weft run whose
  result arrives as a message, and commits `finish` before any waiter is
  answered. A fresh result is answered from the value in hand
  (`SettledFresh`); only a row read back from the ledger is decoded. A `Plane` is `run`, `census`, `children` (adders for a per-scope
  supervisor the host starts through an owner process) and `close`, which is
  handed a function that stops those children. Read its module doc for the
  admission table.
- `remote/owner_link` is the executor's `OwnerServices` over messages: monitored
  calls (`broker/internal/call.try_call`), in-band failure when the owner is
  gone, and a one-cell `Link` that `host.attach` re-points when a later runtime
  attaches to an open scope.
- `remote/owner_port` is the orchestrator's per-session actor. Every request runs
  in a weft run cancelled when its requester exits (`cancel_when_exits`), and a
  `Tail` is served inline. It reconciles acknowledgements on `bind` and on a
  weft periodic timer, acknowledging only keys the injected `settled` accepts.
- `remote/surface` is `attach`, `run`, `recover`, `ack`, `places` and
  `functions`. A lost connection is repaired with `weft/poll.fold_until` on a
  doubling interval and the same `Run` is sent again. `recover` sends
  `QueryOrFence` for a call whose `run.replay` is `ReplayNever` and plain
  `Query` for a `ReplaySafe` one.

Invariants that break things when violated:

- Every session open calls `surface.attach` once, before its first `run` or
  `recover`, and shares that one surface across every strand and every strand
  or runtime restart inside the open. The fresh token is the only fence against
  a dead open's in-flight `Run`; Erlang orders messages per sender pair only. A
  second attach inside one open rotates the token and gets another strand's
  live `Run` refused as `StaleToken`. `recover` assumes the attach ran, because
  "no row" means "never started" only after it.
- A `Run` is idempotent by call key and the host never starts a second run for a
  key. The surface relies on this to re-send after a reconnect; do not re-send
  `Run` anywhere else.
- The host cancels a run only when its last waiter exits with a reason other
  than `noconnection` (`host.cancels_run`). `noconnection` leaves the run going
  and its outcome in the ledger. A waiter that exits with `noconnection` is
  dropped, and its re-send adds it back.
- The ledger row is made `terminal` before any reply, `unknown` before a cancelled
  run's worker is killed, and a request for an `unknown` key never starts the
  call again.
- An escalation crosses as a remaining duration, never a deadline. The plane must
  be built on `AttachSpec.clock` so the executor's deadline and the owner link
  read one time base.
- A `Run` for a scope with no plane in this VM (after an executor restart,
  before the next attach) is answered from the ledger first: a key with a stored
  outcome gets it, a key with an unknown or orphaned-admitted row gets `RunLost`,
  and only a key with no row is refused with `NoPlane` or `PlaneBuilding`. The
  refusal says nothing started, which is false for a call the restart cut off
  (`answer_without_plane`; model: `RefusalMeansUntouched`).
- A scope with no plane in this VM closes as `UnknownCleanup(0)`: without a
  plane there is no retirement witness. A failed plane build leaves the scope
  open for a retry attach at the same incarnation.
- The plane build, a tool run and a close are each a weft run, so none blocks
  the host or another session. While a scope is `Building`, a second `Attach`,
  a `Run` and a `Close` for it are refused with `PlaneBuilding` before the
  ledger is touched; do not move the check after `exec_ledger.attach`, which
  would replace the token the first attach is waiting on. `surface.attach` treats
  that refusal as "ask again": it re-sends the same attach, with the same token,
  until `attach_within_ms` runs out and only then returns `PlaneBuilding`, so a
  repair that re-sends an attach whose build is still running does not fail the
  open.
- A repeated `Close` on a scope that already ended at that incarnation answers
  the stored `CloseOutcome` and does not ask the plane again, so an orchestrator
  whose reply was lost, or whose move resumes after a restart, learns the
  cleanup finished. The scope row is the evidence, not the host's memory. A
  `Close` at another incarnation is still refused. A second `Close` that arrives
  while the first is running is not guarded yet: it finds no plane and records
  `UnknownCleanup(0)` before the first job finishes.
- Recovery of a `ReplayNever` call must fence (`QueryOrFence`). A plain `Query`
  that finds no row leaves a window in which a dead runtime's `Run` is still in
  flight and then starts the call that recovery just reported as not started.
  Making `exec_ledger.query_or_fence` insert nothing fails
  `a_fence_before_a_stale_run_stops_the_run_from_starting_test`.
- The host's death must end the daemon. The planes it builds (pool, executor
  service, broker) are not in its link set, so a lone restart would leave them
  running and build second ones on the next attach. `daemon/main.start_executor`
  unlinks the host and `wait` halts on its monitor.
- `ffi_remote.send` to an unregistered name on the local node raises, so same-VM
  tests must keep the host up; across nodes the send is dropped instead.
- The owner port's requester is a pid on the executor's node, and
  `owner_port.answer` hands it to `weft.cancel_when_exits`. That needs weft
  >= 0.4.6, which watches a remote pid by monitor alone; 0.4.5 raised `badarg`
  from `erlang:is_process_alive/1` and took the session down on the first
  executor callback. The test builds an unconnected node's pid with
  `client_test_ffi:remote_pid/0`.

Tests: `remote/{codec,host,owner_port,surface}_test` run one VM against a real
ledger and a fake plane (`support/remote_fixtures`, which counts how many times
the fake tool ran and can hold one session's build open). `remote/remote_nodes_test` drives
`client_distribution_fixture_ffi`'s `remote_*` scenarios on two real emulators
(`support/remote_nodes` holds the roles): an undisturbed call that round-trips an
owner callback, a connection dropped while the tool runs and repaired at once,
and one that stays down until the tool has finished. Each asserts one outcome and
one tool run. Making the host ignore the attach token fails the stale-token
tests, making `noconnection` cancel fails `only_noconnection_leaves_a_call_running`
and both outage scenarios, and making the surface skip the re-send fails both
outage scenarios.

## Assembling a registered session (protocol 078)

`remote/workspace` is the orchestrator's workspace half. `workspace.attach`
turns an executor's attach reply into the `workspace_plane.WorkspacePlane` that
`serve.assemble_in` already reads, so the conversation half is assembled by the
same code for both placements and only the `Home` (`Here(prepared)` or
`There(placement)`) and the `Half` it yields differ. In order, an open: reads
`remote/scope` and chooses the candidates (the executor the cell names, else the
`Placement`'s, see "Executor pools" below); connects the candidate's peer
(`Reach.connect`, failure keeps the `executor_unavailable:` prefix); writes the
cell, naming the executor and the incarnation; starts one `owner_port` over the
same `OwnerServices` a local session builds; attaches once; compares the census
with the executor's declaration; builds the plane. `serve.assemble_registered` is
the entry point, `daemon/main` keeps the `distribution.Membership` and builds the
`Placement` (`workspace.fixed` or `workspace.pooled`) for a registration with an
`executor` or a `pool`, and `Instance.pool` and `Instance.executor`
are `None` for such a session (`test/support/local_workspace` unwraps them for
local fixtures).

- **Tools.** The registry takes `contributions.described_tools(census.tools)`
  where a local session takes its plane's `decls`, so the order, the pinned
  prompt index and the active list match a local session's. `wiring.run_placed`
  routes by `tool_placement`, unchanged; `plane.run` for a registered session
  ignores the authority `run_placed` read, because the surface reads its own
  with `wiring.read_authority` at the send. `ToolSurface.recover` is
  `Hands.recover`: a workspace tool asks the executor's ledger, an owner tool is
  judged by its replay policy as it is in a local session (safe is offered for
  replay, anything else is unknown). Clearance stays local.
- **Non-tool callers.** Imported hooks, the goal check and Git observation use
  `plane.broker`, a `broker.over(census.broker, clock)` handle, and
  `Half.call_clock`, this machine's clock shifted by the reply's `executor_now_ms`
  (the host reads its clock for each `Attached`, a rebound one included) minus the
  local reading at receipt (`workspace.rebased`). A `CallSpec` deadline is
  absolute and the executor's broker compares it with its own clock, so every
  caller that builds a deadline must read `call_clock`, not `clock`.
  `with_imported_hooks`, `goal_check_wiring` and `worktree_wiring` take it.
- **Prompt.** `render_prompt` is unchanged: the user's guidance is read here, the
  workspace's arrives in `census.prompt`, and the platform, shell and policy
  facts come from `census.census`.
- **Scope record.** `remote/scope` keeps `{incarnation, closed, executor}` in the
  reserved cell `client/remote/scope`. No cell attaches at 1, a close that ended,
  clean or with unknown cleanup, at the stored incarnation plus one, anything
  else at the stored one. The unknown-cleanup case asks for the next
  incarnation so that an operator's `loomd executor release` on the executor
  turns the executor's `UncleanClose` into a reopen. It is written
  straight to the store (the way `session_git` writes), so only while no runtime
  owns it: after the connection and before the attach is sent, by the close
  below, and removed by `scope.clear` after a first open's capacity refusal. A
  `StaleIncarnation` from the executor fails the open naming both numbers, except
  in one case: when the record never saw the close of the incarnation it just
  attached under (`closed: None`) and the executor reports that same incarnation,
  the scope is closed cleanly there. The open then asks the executor to close it
  (a closed scope answers its stored outcome), records the answer, and attaches
  once more at the incarnation the record now allows.
- **Close.** `custody.Workspace` (after `Services`, before `Mcp`) runs
  `plane.close`: ask the executor to close the scope, record what it reports,
  stop the owner port. An unanswered or refused close records nothing and still
  succeeds, since the scope is then open on the executor and the next open
  rebinds it; failing custody over an unreachable machine would only hold the
  reservation. `plane.fatal` is empty, so a partition is not a fatal root.
  `close_instance` stops the broker and helpers only for a local workspace.
- **Settled.** The port's reconciler acknowledges a key when `workspace.settled`
  says this session no longer holds the call pending: the operation's state is
  gone, or its `Tools` batch for that step no longer lists the call as planned or
  running. A staged, interrupted or aborted call is therefore settled; a call
  still running is not. The host retires the row on the acknowledgement and keeps
  a tombstone for the key until the scope's incarnation changes, so a `Run` for
  an acknowledged key, which a dead runtime's effect process can still have in
  flight, is answered `RunLost` and never starts (`Existing(Acked)` in
  `admit_run`; a `Query` reports `Unknown`).
- **Jobs.** `plane.live_jobs` reads the `job/*` cells from the session's store
  and builds the board with `jobs.live_board_of`.
- **Refused or omitted.** Extension tools (`tool_placement` answers `Error(Nil)`,
  so none are registered, with one notice per extension installed on the
  executor) and operator directory additions (`resolve_directory` refuses).
  Owner-bound code-mode capabilities
  (`strand.*`, `notes.*`, `schedule.*`, `peer.*`) are answered:
  `OwnerServices.capability` is `owner_codemode.answering` over the session's
  own Agency, scheduling door and peer wiring, where a local session keeps
  `no_capability` because it never calls it.

What `assemble_in` does not do for a registered workspace, and which is why
nothing touches the registered name: `workspace_plane.prepare` is skipped (it
discovers the toolchain, composes the base, creates the blob store, tool
home, tmp and scratch directories, and `blobs.adopt_legacy` does not run); `start_local` is replaced by the attach (the
helper pool, executor, broker, jobs, scratch, LSP and code-mode host live on the
executor); `code_mode_mcp` is replaced by `started_mcp` over only the
orchestrator-placed servers, whose façades the owner port hands the executor
in its `McpPlan`;
extension
discovery and registration are skipped; `resolve` skips `find_helper` and
`gocache.locate`. The rest reads only the orchestrator's own files (session,
index, memory, home, skills), the census, or the executor through the broker
handle. Owner tools still run `wiring.run_tool` here; with no directory
additions and no filesystem grants for them, `revalidate` has nothing to stat.

Invariants:

- One `Surface` per session open. Do not attach per strand or per driver restart.
- The hook wrappers (`hookserve.wire`, `extension/hooks.wire`) must carry
  `tools.recover` forward; they run after the surface is composed.
- Anything that puts an absolute deadline in a `CallSpec` for the executor's
  broker reads `Half.call_clock`.
- Never write the scope cell while a runtime owns the store.

Tests: `remote/workspace_test` and `remote/scope_test` (real host and ledger, fake
plane via `support/remote_orchestrator`) cover each incarnation shape, token
rotation with recovery, the settled rule, the clock and jobs;
`remote_assembly_test` assembles a whole registered session under custody and
drives a scripted model through a workspace tool and an owner tool.
Routing a workspace tool to the owner path fails `a_workspace_call_runs_on_the_executor_...`,
attaching at the stored incarnation after a clean close fails the reopen tests, and
dropping the clock offset fails `the_non_tool_clock_reads_the_executors_timebase_test`.

## Background code mode and MCP on a registered session (protocol 078 addendum)

The protocol-change/078 addendum "background code mode and MCP façades on a
remote session" is the rule book; `docs/architecture/distributed.md` has the
diagrams. In one paragraph: the orchestrator keeps the execution's durable
record in `async_runs` exactly as a local session does, and the executor only
runs the program, under the ledger key `(session, op, "async/<id>", 0)` with
tool `execution` (`protocol.execution_key`, `protocol.execution_id`).

- **Launch.** `code_mode` with `mode: "launch"` runs on the executor as a tool
  call. Its tool shell sends `OwnerMessage.LaunchExecution(terms)` over the
  owner link; `owner_port.serve` hands it to `Executions.launch`, which is
  `async_codemode.remote`: it claims the record (`Starting`, with
  `Launch(step, source_index)` naming the launching call, F5) through
  `async_runs.launch_fallible`, and the record's worker calls
  `HostLink.start`, i.e. `surface.start_execution`, which sends
  `StartExecution(key, incarnation, token, terms, remaining_ms)` and repairs a
  dropped connection with the same start forever. The host admits the key
  through the same `admit_call` a `Run` uses, runs `Plane.execute` (the
  executor's `codemode.execute` under step `async/<id>`), commits the stored
  value before any reply, and answers `ExecutionFinished`, `ExecutionLost` or
  `ExecutionRefused`. A result larger than
  `Config.execution_result_bytes` (1 MiB, the reservation per row, per
  executor) is stored as an errored value naming its size.
- **Interact.** `check`, `send`, `join` and `cancel` run on the executor as
  tool calls too (`code_mode` is placed there by name) and cross as
  `InteractExecution`, which `async_codemode.interact` answers from the
  record. They wait out a link cut like any tool call; that is a documented
  limit.
- **Owner-bound calls from the program** carry step `async/<id>`.
  `owner_codemode.answering_executions` binds them to the record (it must be
  starting or running, the call's strand and operation, and name its launching
  call) and answers with the arms a local background program gets; a closed
  record is refused `execution_closed`, a missing one `execution_unknown`.
  `owner_link.patience` (F1): `execution.receive*` waits out a cut within its
  own `within_ms` and then answers "no input yet" (`CapOk(NilValue)`);
  `execution.ready`, `progress` and `delivery` wait up to the 120-second
  capability budget and then deny; every other owner-bound call is denied at
  once as before. Waiting is safe only because each of those calls may be sent
  twice.
- **Stop.** A record closes on a recorded decision only (cancel, abort, stop,
  deadline, idle, restart). `async_runs.Wiring.abort` is then
  `Hands.stop_execution`, a `StopExecution` cast: `exec_ledger.stop_or_fence`
  turns an admitted row unknown (`Stopped`) or bars a missing key (`Barred`) in
  one transaction, and the host halts the program with `Plane.abort_step`.
  `noconnection` never stops anything.
- **Reconcile.** `owner_port`'s reconciler lists running executions with
  `HostLink.list` (`Unacked.executions`) and stops each whose
  `Executions.standing` is not `RecordLive`; it acknowledges an execution row
  only when `workspace.settled_by_kind` finds the record `RecordClosed`, so a
  value the worker has not read is never discarded.
- **Recovery.** After an orchestrator restart `async_runs` asks
  `Wiring.surviving_value`, `AskWhere(read)` for a remote session
  (`Hands.execution_lookup`, `surface.query`, 5 s), about each record still
  starting or running: `Lookup.Executed(value)` keeps it finished, anything
  else loses it and stops it. The questions run all at once in a weft run
  beside the service (`ask_survivors`), and their answers arrive as
  `Recovered` messages, so the service answers checks and launches while an
  executor is unreachable; such a record reads as running until its answer
  lands. A local session's `NothingSurvives` loses every unfinished record
  before the service serves any request.
- **MCP placement.** `catalog.McpServer.runs_on` is `RunsOnOrchestrator`
  (default) or `RunsOnExecutor`. For a registered session `serve` starts only
  the orchestrator-placed servers, gives the owner port their façades as
  `McpPlan.served` and the executor-placed names as `McpPlan.expected` (the
  executor's plane build asks for it with `AskMcpPlan`), and logs each
  `remote_census.McpStatus` as `mcp.ready` / `mcp.unavailable` with
  `placement: executor`. The executor starts expected servers from its own
  `[mcp.<name>]` table (`executor_plane.Machine.mcp_servers`,
  `catalog.parse_mcp`), refusing a missing one with "this executor declares no
  [mcp.<name>] table", and answers their calls locally
  (`owner_codemode.over_owner_serving`). The executor's client is filed under
  `custody.Mcp`; closing kills the server at once and a process whose exit is
  not witnessed within 5 s is logged
  `executor.mcp_retirement_unconfirmed`, never counted as `UnknownCleanup`. The
  plan is fixed for an incarnation: a `Rebound` attach keeps the plane's plan
  and census.

Invariants that break things when violated:

- The record is claimed before its worker starts, and every close decision is
  durable before the stop is sent; a stop is always a cast that the reconciler
  repeats.
- Never acknowledge an execution row whose record is live
  (`settled_by_kind`), and never stop a program on `noconnection`.
- `StopExecution` must bar a key with no row, or a start a dead worker sent
  before the record closed runs the program after it was cancelled.
- Only the `execution.*` calls named in `owner_link.patience` may wait out a
  cut; anything not idempotent or keyed keeps the immediate denial.

Tests: `remote/host_execution_test` (admission, finish, stop and fence,
oversized values, restart), `remote/owner_link_test` (F1 over a real link
cut), `remote/owner_port_test`, `remote/workspace_test`
(`settled_by_kind`), `async_runs_test`, `agency_test` (F5),
`owner_codemode_test` (an executor-placed server answered locally),
`catalog_test`, `mcp_test`, `remote/codec_test`; across two shipped daemons,
`daemon_shipped_remote_background_test` (input, progress, a cut link, an
executor restart, an orchestrator restart that keeps a finished value) and
`daemon_shipped_remote_mcp_test` (each placement and a missing executor
table). The P model `protocol/models/remote-execution` covers the execution
path with mutants M8 to M12.

## The executor role (protocol 078)

A `loomd` whose `--config` file has `[distribution]` and at least one
`[workspaces.<name>]` row serves those checkouts to the peers it pins.

- `client/workspaces` decodes `[workspaces.<name>]` (`root` only: absolute, no
  `..` segment) the way `client/executors` decodes executors, and requires
  `[distribution]`. `catalog.parse` validates it; `catalog.parse_lsp` reads only
  the `[lsp.<name>]` tables because an executor has no model catalogue. A new key
  means `workspaces.row`, `scripts/config_keys.sh` and `docs/configuration.md`
  move together.
- `client/executor_plane` is the real `PlaneFactory`. `machine` reads this
  executor's own settings once (`--helper`, `--config`, `--codemode-seed`,
  `--best-effort`, `--full-enforcement`, `--read-scope`, `--network`,
  `LOOM_HELPER_POOL`, `LOOM_DISABLE_TOOLS`, and the `[tools]`, `[workspace]`,
  `[lsp]`, `[jobs]`, `[secrets]` tables). `factory` resolves the workspace name
  against the rows, canonicalizes the root, creates `<state>/scopes/<session>`
  fresh (the helpers' scratch lives there; the session name is validated before
  it names a path), creates the session's blob store (below), and calls
  `workspace_plane.prepare` then `start` with the
  `OwnerServices` from the `AttachSpec`, `owner_codemode.over_owner` as the
  owner's code-mode arms and `codemode_wiring.seam` with
  `owner_codemode.advertising_peers` as the `code_mode` tool. The census it
  returns is
  `RemoteCensus` (plain data plus the broker's subject; the prompt facts are read
  once, at attach). `workspace_plane` itself is unchanged apart from `WorkspaceSpec.blob_root`.
- **A remote session's blobs.** They live in `<state>/blobs/<session>`, passed as
  `WorkspaceSpec.blob_root`. Not in `<state>/scopes/<session>`, which is removed
  when a scope retires cleanly while a reopened session must still read the
  blobs its transcript names; and not in the checkout, for the reason the
  daemon keeps its own out of the workspace. `blob_directory` validates the
  session name like the scope directory does and creates the store private if
  it is not there, adopting it if it is, and nothing removes it. The base the
  factory composes carries `workspace_policy.protecting_blobs` for it, so every
  jail on the executor masks it (the state-root masks name the entries the
  daemon writes, not this directory), and `tools/fs` still reads it. The
  census carries it (`Census.blob_root`), so the owner sees the root. No
  `adopt_legacy`: it copies what an earlier release left in a workspace, and
  no release wrote a registered session's store into a checkout.
- The plane's cleanups cannot go to custody, because the factory runs in a
  short-lived weft run. The `Retain` it passes files each `(Part, cleanup)` in the
  build process and calls `transfer` (unlinking the build from the resource).
  `retire` runs them in `instance_owner.Part` order after the language servers
  and the scope's children, and reports `AllRetired` only when nothing failed and
  the `Helpers` cleanup (`executor.close`, which ends in `exec.close_pool`)
  succeeded. No helper cleanup means `UnknownCleanup`. The scope directory is
  removed only on `AllRetired`.
- `daemon/main.start_executor` starts `remote/host` under `address.default()`
  with the ledger at `<state>/exec-ledger.db`; a daemon with no rows starts
  nothing. The host is unlinked and monitored; its death halts the daemon.
  Rows are checked to be directories at startup (`existing_roots`) and again at
  each attach.

Tests: `executor_plane_test` runs the factory over a temp checkout with the
shipped helper (write then read, `bash`, close then reopen, a blob written in
one incarnation read after the reopen and kept out of the checkout, an unknown
workspace, the census walk for function values and its `term_to_binary` round
trip, an 8 MiB read, the `code_mode` description listing the owner-bound
capabilities where a seed exists, a real program whose `strand.roster` and
`notes.put` reach the owner's Agency through the owner port where the host
has a jail, and `retire` with injected cleanups). `daemon_executor_test`
covers the boot decision. `remote_nodes_test`'s `remote_workspace` scenario
serves a real workspace to a second node. Making `retire` ignore the helper
witness fails `a_scope_with_no_helper_cleanup_has_no_witness_test`; building the
plane back inside the host actor fails the two build-isolation tests in
`host_test`.

## Executor pools (protocol 078)

A session may be created in a `[pools.<name>]` instead of on one executor
(`sessions.create{pool}`, exclusive with `executor`). The executor is chosen when
the session first opens and then never again.

- `client/executors` also reads the optional declarations of a row: `platform`
  (`<os>/<architecture>`, the label `system_prompt.platform` writes),
  `enforcement` (`enforced` or `degraded`, the census's `PromptFacts.helper`) and
  `toolchains` (`codemode` or an `[lsp.<name>]` key). They are the operator's
  claims. `executors.contradiction(row, observed)` compares them with what an
  attach census says and words both values; a row that declares nothing, or that
  provides more than it declared, is never contradicted. `client/pools` reads
  `[pools.<name>]` (`executors` in trial order, plus the same three keys as
  requirements) and `pools.candidates(pool, configured)` is the whole filter, a
  pure function of configuration: an executor that declared nothing cannot
  satisfy a requirement. `catalog.parse` validates both tables.
- `workspace.attach` is the placement loop (read its module doc for the order of
  steps). The scope record names the executor, and it is written after the
  connection and before `Attach` is sent. That one ordering is what makes "a
  session whose record names an executor has only that candidate" structural, and
  a reply lost after delivery is retried against the same machine, where the
  ledger's `Rebound` arm makes it converge. A first open (no record) goes to the
  next candidate on exactly two outcomes, both of which prove no scope exists: the
  connection failed, or the executor answered `CapacityExhausted` (decided inside
  the attach transaction before any plane is built). After the capacity refusal
  the record is cleared. A capacity refusal at reopen, an unanswered attach, a
  version or workspace refusal and a build failure all fail the open and leave the
  record where it is. The census is compared with the declaration after the attach;
  a contradiction asks the executor to close the scope and fails the open.
- `workspace.Placement` is three functions so that the configuration and the
  catalogue stay outside this module: `first` (the candidates for a session with
  no record, or why there are none), `named` (a configured executor by name, for
  a record that names one) and `chosen` (told the executor after every successful
  attach, because an earlier open whose reply was lost may have named it without
  the catalogue hearing). `workspace.fixed` and `workspace.pooled` build them.
  `chosen` is `manager.seed_executor`, a cast that runs `catalogue.seed_executor`
  in the registry's turn, which writes the `executor` column once, only for a
  registration with a pool and no executor.
- The catalogue is at version 15 (v13 adds `catalogue_session_moves`, the
  custody of a session moving between orchestrators, read with
  `catalogue.custody`; v14 adds the deletion mark; v15 adds `catalogue_delegate_sessions`, read by the
  registry's `movable` check and written by `manager.delegate_session`).
  `Registration.pool` is part of the creation
  request and `reserve_creation` compares it, and for a pooled request it does not
  compare the executor, which is the first open's choice (`named_same_executor`).
  `view_json` adds `executor` and `pool` members only when non-empty. The control
  decoder refuses `executor` with `pool`, a `workspace_private` scope and a
  workspace that is not a registered name; `server.create_session` answers
  `pool_unknown` for a pool the daemon does not configure.
- `LOOM_EXECUTOR_MAX_SCOPES` (`executor_plane.scope_limits`) lowers the number of
  scopes an executor admits before refusing an attach; the ledger default is 16.
  It is how a test makes an executor full.

Tests: `remote/placement_test` runs real hosts and ledgers, each with a limit of
one scope and its own name, in one VM, and covers the order, a connection that
fails, a pool full everywhere (the record is withdrawn), a reopen into a full
executor, a bound executor that is down, a link that breaks after the attach was
delivered (`remote_orchestrator.lossy_link`), and declarations the census
confirms and contradicts. `daemon_registered_test`, `daemon_protocol_test`,
`executors_test` and `pools_test` cover the wire and the tables, and
`daemon_shipped_remote_pool_test` boots an orchestrator and two executors with
`support/remote_daemons.trio`. Dropping the record write before the attach fails
five tests of `placement_test`, and letting a reopen treat a capacity refusal as
"next" fails `a_reopen_into_a_full_executor_never_moves_to_another_test`.

## Two orchestrators (protocol 078, phase 3)

A deployment may run two orchestrators, each with its own catalogue. A session is
created on, and owned by, the orchestrator the client is connected to, and a
daemon asked about a session its catalogue lacks asks the others which one holds
it. The decision is the owner's option C: a Khepri-shaped directory interface
backed, for now, by a parallel lookup over pinned peers; each catalogue stays the
source of truth; phase 5 added the write half (`activate`) and kept the catalogues
as the authority (the next section); failover is deferred.

- `client/orchestrators` decodes `[orchestrators.<name>]` (`node`, which must be a
  `[[distribution.peers]]` node as an executor's must, and an optional `address`
  held to `host/claim.remote_address`, so a bearer never goes over cleartext to a
  remote host or into a URL with credentials). Two names for one node are
  refused. `catalog.parse` validates it and `daemon/main.prepare_startup`
  captures it in `Config.orchestrators`; a new key means `orchestrators.row`,
  `scripts/config_keys.sh` and `docs/configuration.md`.
- `remote/address` is `Address(message)`: the node and a `Name(message)`, no longer
  pinned to the executor host's `HostMessage`.
- `remote/orchestrator_port` is the answering end: one weft actor per daemon under
  the fixed name `loom_orchestrator`, with its own closed `Message` (`Owns(session,
  reply)`) and `Ownership` (`Owned | NotOwned`). It reads the catalogue through a
  function `daemon/main.catalogue_holds` builds over `manager.get`: a registration
  in any state and either visibility is `Owned`, a missing one `NotOwned`, and a
  read that fails sends nothing, so the asker's deadline reports it as unreachable
  and a fault is never mistaken for proof. `daemon/main.start_orchestrator_port`
  starts it, linked to the process that runs the daemon's services, whenever
  `[distribution]` is present, whether or not the daemon lists any orchestrator.
  `ask` takes the monitor before it sends, as `surface` does, so a node that is
  already gone ends the wait at once.
- `client/session_directory` is the interface: `Directory(lookup)` answering
  `Ok(Here)`, `Ok(Elsewhere(Orchestrator))`, `Error(Unknown)` or
  `Error(Unreachable(names))`. `peers` is the phase 3 backing: the local
  catalogue first, then every configured orchestrator at once in one weft run
  under `deadline_ms` (2 s), each task `distribution.connect` then
  `orchestrator_port.ask`. `decide` is the pure policy: the first holder in
  configuration order wins over any silence; with no holder, any silence makes the
  miss `Unreachable`; only a full set of "not held" is `Unknown`. A callback that
  crashes or runs past the deadline is silence. Nothing is cached, registered or
  retried, and no connection is made at startup. One command retries:
  `peers.link` has no outbox and no client retry behind it, so a link whose
  lookup finds the owner silent calls `Directory.settle` (`settle_over`: every
  listed orchestrator connected at once under `settle_ms`, 5 s) and asks once
  more, because the silence may be a first TLS handshake that outlasted the
  lookup's `connect_ms` (1.5 s) and finishes in the background
  (`daemon_shipped_peer_mail_test`'s slow-handshake test).
- `daemon/server` takes the directory as `Config.directory`
  (`session_directory.none()` when there is no distribution or no orchestrator).
  `dispatch` runs `sessions.get` and `sessions.open` as it always did and, only
  when the answer is `not_found` and the principal is the owner, asks the
  directory (`redirected`): `Elsewhere` becomes `not_owner` with `orchestrator`
  and the row's `address`, `Unreachable` becomes `owner_unreachable` with
  `orchestrators`, and `Here` or `Unknown` leave `not_found`. A member is always
  `not_found` and the directory is not asked, because a member's standing is the
  owning daemon's to judge; a stale epoch is refused before the lookup. Control
  refusals are the `Refused(code, message, detail)` record, whose `detail` is
  empty except for these two, and `refusal_with` writes the members.

Not built, on purpose: a merged `sessions.list`, a `register` step (ids are UUIDv7
and creation is local), a `not_owner` on the session socket, an address advertised
by the owner, any automatic follow (a client would need a credential for a second
daemon), and a new constructor on `remote/protocol.HostMessage`.

Peer mail across orchestrators (protocol-change/078, the peer mail addendum). The
only same-node assumption in peer mail was the recipient's `peer_mail.Endpoint`,
so the work is a second kind of endpoint and a directory that returns it:

- `orchestrator_port.PeerCommand(session, command, reply)` is a second `Message`
  beside `Owns`. `served` names every `peer_mail.Command` constructor and accepts
  only `Allow`, `Revoke`, `Deliver`, `SentReceipt` and `Roster`; any other
  (`Inbox`, `History`, `Link`, `Describe` (the write of the recipient's own
  self-description), `Outbox*`, ...) is answered `Refused(peer_unserved)`
  without calling the handler, and a new `Command` constructor is a compile error
  there until someone decides. The handler is the `start_serving` argument;
  `start` keeps its signature and refuses every peer command.
  `daemon/main.peer_command` is the production handler: `manager.resolve`, then
  the resident's own endpoint. A session not resident here answers
  `peer_mail.not_open_reason` when this catalogue holds it and
  `peers.not_running` when it does not. The command is plain data. The port runs a
  command in its own loop, so a wedged session delays the next `Owns` by at most
  the Agency's 5 s holder timeout.
- `orchestrator_port.Describe(session, reply)` is a third `Message` (a constructor
  an older port lacks raises `case_clause` and halts it, and a new
  `peer_mail.Command` would too, since `served` has no catch-all, so the port
  assumes same-build peers): what this
  daemon's catalogue says about a session, answered in the port's own turn by the
  `start_with` argument `Describer` (`server.local_description`, which is
  `manager.get` and `view_json`, and `moved` for a tombstone). It never opens the
  session, so a saved one is described. `start`, `start_serving` and
  `start_importing` refuse every description with `peer_unserved`.
  `orchestrator_port.ask_description` is the asking end.
- `remote/remote_peer` builds the other end. `at(address, session, within_ms)` is
  an `Endpoint` whose `call` monitors the port (`address.watch`) before it sends,
  then waits for the answer, the `DOWN` or the deadline: an answer is `Ok` or
  `Refused`, except that the text `peer_mail.not_open_reason` is `NotOpen`
  (`failure_of`, the one place the wire text becomes the variant);
  `noconnection`, `noproc` and the deadline are `Unreachable`. `wait_ms` is the
  bound: `read_ms` (2 s) for a `Roster`, `call_ms` (7 s) for everything else.
  `over_distribution(membership)` resolves the pinned peer, connects (1.5 s) and
  asks with `call_ms` (7 s). A reply after the deadline reaches nobody, which is
  the lost-reply case the outbox expects: the retry gets `same_receipt`.
- `session_directory.Directory` has a second field, `reach(orchestrator, session)
  -> Endpoint`, and a third, `describe(orchestrator, session) ->
  Result(JsonValue, String)`. `none()` and `peers()` give an endpoint that is
  always `Unreachable` and a description that is `owner unreachable`;
  `with_reach` and `describing` set them, and `daemon/main.session_directory_of`
  sets `remote_peer.over_distribution` and `session_directory.description_over`
  (2 s once connected).
- `peers.routed(local, sessions)` is the one `Directory.resolve` both
  `server.peer_directory` (control commands, the web page) and
  `main.peer_directory_across` (a session's tools and drainer) use. A session
  resident here is answered by `local` and the directory is not asked. Only a
  miss calls `lookup`: `Elsewhere` gives `reach`, `Unreachable` gives
  `peer_mail.Unreachable`, and `Here` or `Unknown` keep `local`'s failure. `local`
  is `server.local_peer`, which knows the difference: `NotOpen` when this
  catalogue holds the session and it is not resident, `Refused` when it does not.
  `main.peer_directory` keeps its two-argument form with `session_directory.none()`.
  `peers.described(local, sessions)` is the same shape for `Directory.describe`:
  this catalogue's row first (`server.local_description`), otherwise the owner's
  through `sessions.describe`, `owner unreachable` when no owner could be asked.
- `LinkPeers` resolves its target through the directory, so `peers.link` writes
  the grant on the recipient's orchestrator (`Allow`). `unlink_session` revokes
  there (`Revoke`), and an unreachable revoke is reported in `recipient_grant`.
  `peer.sent_receipt` asks the recipient's orchestrator (`SentReceipt`) when the
  sender's own row has no receipt.
- Peer mail reach (the peer mail reach addendum). A recipient on another
  orchestrator lists as a local one does. `peers.roster` asks the owner's
  `Roster` and reads `Error(NotOpen)` as `running: false` with `exported_strands`
  null; an owner that does not answer leaves `running: true` and an `unavailable`
  object, and metadata `unavailable` too, so one silent owner never fails a
  listing. `peers.inspect` takes `wake` from the same `Roster`. The owner lists
  only the strands the recipient granted the asking session, the sender asks only
  about sessions its own links and grants name, and a default link
  (`[peers] default_links`) joins the sessions of one daemon only.
- A recipient its owner holds saved is `NotOpen` and is queued, not refused, here
  and on another orchestrator alike (`server.standing` classifies a non-resident
  session as `Unopened`, `Handed` (a tombstone; waits, so the next lookup follows
  the move), `Archived` (refused: opening needs a restore) or `Unheld`): `peers.deliver` maps it to
  `peer_outbox.NotOpen`, the row stays `pending` waiting for an open, and the
  send answers `peers.queued_unopened`. Each attempt resolves the recipient
  again, so a moved session is followed to its new owner and a deleted one is
  refused `not_running`. protocol-change/077 refused this; its addendum in 078
  records the change. The drainer's backoff and the two refusal texts are in the
  sender outbox paragraph above.

Tests: `orchestrators_test` (the table), `remote/orchestrator_port_test` (the
answers and the silence), `session_directory_test` (the policy, the fan-out's
concurrency and its deadline, a crashing question), `daemon_directory_test` (a
stub directory behind the real control socket: which commands ask, which
principals are redirected, what the refusals carry; and `catalogue_holds` against
a real registry including a reserved and an archived row),
`daemon_shipped_directory_test` (two shipped daemons), `peer_remote_test` (two
real ports in one VM: link, send, receipt and unlink across them, the refused
commands, an unreachable then returning owner under the drainer, a lost reply,
the roster and description of a resident and a saved remote session, a hidden or
unlinked one staying hidden, a send to a saved remote session waiting then
delivered once, expiry, deletion and a move while waiting),
`daemon_peer_lookup_test` (the real registry: `local_peer`, `peer_command` and
`local_description` for a held, an unheld and a tombstoned session), and
`daemon_shipped_peer_mail_test` (two shipped daemons: a send to a stopped
orchestrator is queued and delivered once, and a session stopped on the other
orchestrator is described, listed, queued to and delivered to once it is opened,
over the fixture in `support/remote_duo`).

## Moving a session between orchestrators (protocol 078, phase 5)

An owner hands a session on an executor to another orchestrator with
`sessions.move`. Two catalogue rows are the authority, the source's `moving` and
`moved` and the receiver's `imported`, ordered by a write-ahead intent. No third
store decides and the executor records no owner; its incarnation fence is a second
guard, because a copied cell makes the receiver attach at `incarnation + 1` and
the executor refuses the old token by value. The storage half is in
`packages/storage/CLAUDE.md` (`Custody`, `export_closed`, `release_export`,
`domain.import_session`). `docs/design-notes/distributed-runtime.md` section 8 and
the protocol-change/078 addendum on moving a session have the protocol.

- `session_move` is the vocabulary and nothing else: the six `Step`s, the `Manifest`
  (the registration without its path and configuration, which name files on the
  source's machine), `Chunk`, `Activation`, `Stage` (`Absent | Received |
  Activated`), `Verdict` (`Accepted | Refused(Refusal) | Failed`) and the file and
  lease-owner names. It performs no I/O, so the mover, the importer and the port
  import it without importing one another.
- `daemon/manager` carries the catalogue half. `begin_move` commits `moving` and
  cancels the slot in one registry turn, so nothing can open the store after the
  intent. `prepare_slot` reads the custody row and refuses `moving` and `moved`
  (`SessionMoving`, `SessionMoved`); archive, restore and delete refuse them too
  (`AdminMoving`, `AdminMoved`). `movable` refuses a session with no executor, an
  archived one and a reserved one (`AdminNotMovable`). `finish_move`, `abort_move`,
  `moving_sessions` and `custody` are the mover's reads and compare-and-sets, and
  `import_session` registers, records the import and renames the copy into place
  in one turn, finishing the rename for a repeat and removing a late duplicate
  instead of putting it over the placed file. It reads whether the import already
  committed before it checks for an open slot, and `AdminBusy` refuses a first
  import only.
- `remote/orchestrator_port` gains `Import`, `ImportStatus` and `Activate` beside
  `Owns`, and `Ownership` gains `Moved(to)`. The daemon supplies an `Importer` of
  three functions, so the port knows neither the catalogue nor the filesystem. A
  daemon that never receives sessions starts the port with `declining()` and
  refuses all three. Pieces and the status run in the port's own turn, so the
  pieces of one copy land in order; `Activate` hashes and opens a whole file, so it
  runs in a task linked to the port and cancelled with its requester, and `Owns` is
  never kept waiting behind it.
- `session_directory` is `Directory(lookup, activate)` and `Courier(send, stage)`.
  A catalogue's own tombstone answers `lookup` as `Elsewhere` with no question to a
  peer, and a peer's tombstone redirects when nobody holds the session. The
  courier and `activate` are built over the pinned connection, one connection
  attempt per call, as `over_distribution` is.
- `session_importer` is the receiving end. `take` writes pieces to a `.part` file
  that becomes the copy only when the last piece lands; a piece that does not
  start where the file stands is `OutOfOrder` and the sender begins again.
  `activate` asks the registry first, and a repeat of the same move is answered
  `Accepted` without looking at the copy or at who asks, using the origin the row
  recorded (an operator who renamed the sender's `[orchestrators]` row meanwhile
  changes nothing). Only then does it check the sender's node against
  `[orchestrators.<name>]`, then the digest, then the scope cell read from a
  scratch copy (opening a session file rewrites it), then the executor row, and
  only then hands the registry the import. A copy refused for a reason a resend
  cannot cure is removed. `stage` returns `Result(Stage, Nil)`: a registry read
  or a copy path that cannot be examined is `Error(Nil)` and the port sends no
  reply, because `Absent` would make the source send the file again to a receiver
  that may hold the session, and `Activated` over an unexamined path could let the
  source retire while a copy still waits.
- `session_mover` is the source's driver. `drive` takes the steps in order and every
  run starts from what is on disk: it reads the row, asks the receiver how far the
  move has got, and does what remains. A move finishes, is abandoned only on an
  answer, or stalls. `Abandon` comes from an unproven cleanup, an executor that
  refused the close, a corrupt or oversized file, a receiver's `Refused` and
  nothing else; silence is `Stall`, because an unreachable receiver may have
  activated the session. A digest the receiver refuses, or a copy it no longer has,
  sends the whole file once more. A send that finds the receiver's stage
  `Activated` goes straight to the retirement. Each step is bounded by a weft deadline of its
  own (`Budget`), and an expiry is a stall. `Environment.after` is told after each
  step; a daemon passes the crash knob and a test passes a recorder.
- `session_movers` is the daemon's actor, started beside the orchestrator port and
  linked to the process that starts the daemon's services. `Control` carries the
  listed orchestrators, `begin` and `holds` (the ask of an origin's port). One entry per session;
  `Begin` starts a mover once, a tick runs a stalled one again, `resume` begins every
  `moving` row at boot. It is not the registry, whose turns are bounded by five
  seconds while a move waits an executor's minute.
- `remote/workspace.close_stopped` closes the scope of a session that has no
  runtime and tells `CloseRefused` (final) from `CloseUnanswered` (retried). A
  repeated Close at a closed scope answers the stored outcome (`remote/host`), which
  is how a restarted mover learns a lost reply had landed.
- `daemon/server` has `sessions.move` (owner, epoch, destination in
  `Config.movers.orchestrators`, then `inbound_settled` for an imported session,
  then `manager.begin_move`, then
  `Config.movers.begin`), the `moving` and `moved` members of `sessions.get`
  (`with_custody`) and of every `sessions.list` record (`listed_json`, which reads
  one `manager.custodies` for the page and encodes with the same
  `custody_members`), `moving` on `open` (`in_flight`), and `not_owner` from the
  tombstone for open, archive, restore and delete (`tombstoned`). `daemon/main`
  builds the movers in `start_movers`, gives the port its importer, and reads
  `LOOM_MOVE_CRASH_AFTER`.

`LOOM_MOVE_CRASH_AFTER=<step>` is **test-only**: it halts the VM the moment the
named step (`intent`, `close`, `cut`, `send`, `activate`, `retire`) is durable,
so `daemon_shipped_remote_move_test` can lose the source at each step. It is read
once at startup, is not a configuration key, and is not an operator setting.

Invariants that break things when violated:

- Once the receiver has committed `imported` under an operation, every `Activate`
  for it is answered `Accepted`, whether or not a slot is open, the sender is still
  listed, or the copy is still there. The source abandons on any refusal other than
  a bad digest or a missing copy, so a refusal after the commit leaves two owners.
  `Move.tla` models it as `RefuseUncommitted`, with `MutantRefuse` as its check.
- A move is begun on an `imported` session, and `sessions.delete` removes one,
  only after the orchestrator it came from answers `Moved` for it
  (`inbound_settled`, through `Control.holds`, outside the registry turn; a move
  is refused `not_movable` and a delete `busy`). `begin_move` replaces the
  `imported` row and `abort_move` deletes it to `resident`, so a move begun
  earlier loses the record the source's retry needs, and a return move meets the
  source's own `moving` row. A delete removes the row, so the retry imports the
  session afresh. Both daemons would abandon and both would be resident. The
  web page's delete in `ui_socket` does not make this check yet. A session
  directory member does not apply this hold (next section): the record decides,
  and the origin's abandon expects its own `moving` record, so it fails once the
  receiver activated.
- The intent is committed in the registry turn that cancels the slot, and admission
  reads the custody row in the turn that reserves a slot. Moving the read out of
  that turn lets a runtime open a file whose copy is being cut.
- Only an answer abandons a move. An unreachable receiver, an unanswered close and
  an expired deadline are stalls. Abandoning on silence can leave two owners.
- The activation is the compare-and-set on the receiver and the retirement is the
  one on the source; `moved` has no way out under the same operation, and a
  returning move needs a new one.
- The receiver reads the copy's cell from a scratch copy and never from the file it
  hashed. A path that holds no file is refused before `scope.read_at` opens it,
  because opening creates a missing file.
- A returning session keeps the registration and mapping its catalogue already
  has; only the custody row changes.
- The cut reclaims its own lease and nothing else's. `sqlite.release_export` frees
  it on abort and before the original is set aside, so a refused move never makes
  the owner wait out ten minutes.

Not built, on purpose: a merged list, an owner column on the executor, resume inside
a file, abandoning after a send except on a refusal, failover, a local-session move,
memory-domain, membership and claim transfer, and a web surface.

Tests: `session_move_test`, `session_importer_test` (the pieces, every refusal, a
return trip), `session_directory_test` and `remote/orchestrator_port_test` (the new
answers and messages), `daemon_manager_test` (the registry's half),
`remote/workspace_test` and `remote/scope_test`, `session_mover_test` (two
registries, the real port and an executor host in one VM: every interruption, the
trip there and back, the movers' retry and resume), `daemon_move_test` (the command,
views and refusals across the real control socket), and
`daemon_shipped_remote_move_test` (three shipped daemons, a clean move and the
source lost after each step). The terminal's side is `tui`'s `sessions_move_test`.

## Subagents on another orchestrator (protocol 084, slices S1 to S3)

Slice S1 of phase 1 is the policy and the messages. Slice S2 is the box's side:
the delegate session and the Agency that serves a remote parent. Slice S3 is the
laptop's side: the stub, the sending end, the second drainer, and the Agency's
spawn, wait and roster. A remote child is always detached, so the laptop has no
run-end path. The deadline tick and the retention sweep are
S4, and the shipped two-orchestrator test is S5.

- `spawners` is the `[spawners.<name>]` table, read by `daemon/main.prepare_startup`
  into `Config.spawners` and validated by `catalog.parse` too. A `Spawner` has its
  `node` (a pinned `[[distribution.peers]]` node no other spawner uses), one
  `Placement` (`OnExecutor`, `InPool` or `InDirectory`, resolved against the
  executors and pools), a `profile` that must be a `[profiles.<name>]`, a `tools`
  ceiling (`default_tools` is every built-in tool except `agent_spawn`,
  `remember`, `peer_*` and `schedule_*`), and `max_depth`, `max_sessions` (at most
  16), `max_within_s`, `default_within_s` and `retain_s`. The decoder refuses a
  `default_within_s` above `max_within_s` and a `retain_s` below it, naming both
  values, because either may be a default the file never wrote. `find` and
  `by_node` resolve a spawner; the port uses `by_node` on the node a message came
  from. A new key means `spawners.row`, `scripts/config_keys.sh` and
  `docs/configuration.md`.
- The orchestrator name grammar is the profile grammar, which excludes `@` and `#`.
  A remote handle's parse (`{strand}#{operation}@{orchestrator}`) depends on that,
  and `orchestrators_test` pins it.
- `remote_spawn` is the plain-data vocabulary: `Parent`, `Origin` (the sender's
  node beside the parent), `CallSite`, `Request` with `Detach`, `Spawned`, `Joined`
  (`Ready | Pending`), `ChildUsage`, and `SpawnRefusal` with `Cap`. It reuses
  `tools/agent`'s `Handle`, `Minter`, `Outcome`, `TerminalResult` and
  `ResultSchema`. S1's temporary `Unimplemented` refusal is gone. The module also
  holds the pure half of taking a message to the Agency: `Terms` (the spawner's
  figures in milliseconds), `Delegation` (the parent, its depth and tools, the
  terms), `agent_request` (judges the deadline and the tool names against the
  terms and builds `tools/agent.SpawnRequest`: a deadline is always finite, above
  the cap is refused and not clamped, a name outside the ceiling is
  `ToolOutsideCeiling`, context is always fresh), `clamp_tools` and `refusal_of`
  (the Agency's refusals as the sender reads them; the one cap refusal is named
  by the number it hit, and a tie is `FanOut`). `join_cap_ms` (25 000) is the
  longest one join waits.
- `orchestrator_port.Message` gains `Spawn`, `Join` and `RunEnded`. `answer` runs
  each through `detached`, as it does `Activate`, so none of them delays an
  `Owns`. `detached` now hands its computation the requester's node
  (`ffi_remote.node_of`, a stock `erlang:node/1` binding: there is no pure way to
  ask where a pid lives). The functions behind the messages are a `SpawnHost`
  that `start_with` takes as a fifth argument. `refusing_spawns()` is the host of
  `start`, `start_serving` and `start_importing`, and answers `UnknownSpawner`.
  `daemon/main.spawn_host(config.spawners, delegates)` is the production host: a
  node no spawner names gets `UnknownSpawner(node)`, and a configured one is
  served by the `agency.DelegateEndpoint` that `delegates(spawner, parent)`
  returns (`delegate_host.resolver` in the daemon). The spawn carries the
  spawner's `delegate_host.terms_of`. A host that crashes sends nothing, so the
  asker's deadline reads silence. `ask_spawn`, `ask_join` and `ask_run_ended` are
  the thin sending ends the tests use; slice S3 builds the real one on
  `remote_peer` and uses the first two. Nothing sends `RunEnded` in phase 1.
- The delegate session (slice S2). `delegate_host.resolver(Context)` is the step
  between the port and an Agency: `manager.delegate_session` finds or creates the
  session of a (spawner, parent session) pair in one registry turn, under
  `max_sessions`, through `create_scoped` with `SessionOnly`, with a request key
  derived from the pair, and records it in the catalogue
  (`catalogue.claim_delegate`). The resolver then waits (`open_budget_ms`, 20 s,
  `weft/poll`) for the session to become resident and returns its
  `DelegateEndpoint`. The endpoint is a field of `serve.Instance` and
  `serve.Resident` (`spawns`), three closures over the Agency holder's name and a
  trimmed `Config`; keep them small, because `manager.resolve` copies the
  projection to the caller. A placement is `creation`: an executor or pool
  workspace is kept as written, a directory is canonicalized.
  `movable` in the manager answers `AdminNotMovable` for a session with a
  catalogue delegate row. Not built in S2: refusing members on a delegate session.
- `agency` serves a remote parent through `delegate_spawn`, `delegate_join` and
  `delegate_run_ended`, each a call into the holder or a wait outside it, and
  `delegate_endpoint` projects the three. `spawn_on` takes a private `Source`
  (`Local | Delegated(Delegation)`). A delegated caller is the reference `^main`
  with the call site's coordinates, so `child_name` and `adopt` run unchanged. For
  a delegated source: the depth is the message's plus one against the spawner's
  `max_depth`; the parent's tools are the message's, clamped to the ceiling before
  the holder sees them; the base configuration is the session's own `main`
  strand; the brief's origin is `PeerOrigin(parent session, parent strand)`. The
  ended fence `delegate/ended/{operation}` is read by `admit_run` only on the path
  that creates or finishes a child, not on adoption, and written by `run_ended_in`
  inside the holder before `reap_run_in`. `run_ended_in` counts the runs the
  operation owns that were `Unstopped`, so a repeat counts nothing. `delegate_join`
  cuts its window to `join_cap_ms`, waits as `^{strand}` (`wait_for`, which `wait`
  also uses) and adds each settled child's usage from `child_usage.of_strands`
  (one ledger scan, rows summed by branch entry). Grandchildren inside a delegate
  session are still bounded by the Agency's own `depth_cap`, not `max_depth`.
- `runtime/api` refuses a strand name starting with `^`
  (`InvalidStrandName`), in `seed_strand`, and reserves the `delegate/` fact
  prefix.

The laptop's side (slice S3):

- `remote_child` is the parent's durable record. `Stub` is `remote-child/{call
  site digest}` (`agent.call_site_digest` of the caller), written by `admit` before
  anything is sent and holding the orchestrator, the parent strand, the call site,
  the parent's depth and tools, the whole `remote_spawn.Request` and the child's
  derived name. Its `State` is `Requested` (the spawn is owed), `Spawned(Placed)`,
  `Settled(Placed, Outcome, ChildUsage)` or `Refused(reason)`. `admit` expects the
  cell absent and returns `Existing` for the stub already there. `spawned`,
  `refused` and `settled` are compare-and-set steps that leave a stub alone once it
  has left the state the step starts from: a late `Spawned` never undoes a
  `Settled`, but it does move a `Refused`, because an answer that names a child
  means the child exists. `spawned` writes the lineage cell
  (`orchestrator: Some(..)`, `detached: True`) before the stub moves. A refusal
  never removes a stub: the effect process that hears one and the drainer both
  record `Refused`, so a later answer from the other asker of the same call site
  still has a stub to move, and the child is never left with a lineage cell and no
  stub. `is_live(stub, now)` is true for `Requested`, and for `Spawned` until
  `placed.deadline_ms`, so a stub frees its slot by the clock with no network.
  The decoders are total. The only reserved prefix is `remote-child/`.
- A remote child is always detached. `agency.remote_request` sends `Detached`
  whatever `detach` the model gave, and the child outlives the run that asked for
  it, bounded by its deadline and the box's retention sweep. Nothing on the
  laptop sends `RunEnded` in phase 1: `reap_run` aborts the local children and
  stops there. The port's `RunEnded` message and the box-side fence are S2's and
  stay, for the phase that releases children.
- `remote_reach.Reach` is what the Agency asks through: `session`, the
  `orchestrators` it may name, `offered` (asked at each spawn: the owner holds the
  session alone) and the two functions `spawn` (which also takes the most the call
  may wait, in milliseconds) and `join`, each answering `Ok`,
  `Error(Refused(reason))` or `Error(Unreachable)` as peer mail's endpoints do. It
  is its own module because `orchestrator_port` sits above the Agency in the
  module graph (`notice -> owner_services -> remote/protocol -> remote/address ->
  orchestrator_port`); the Agency cannot import the port. `spawn_ms` (30 s) is the
  longest one spawn call waits. `remote/remote_spawner.over_distribution` is the
  production reach: it resolves the name through the configured `[orchestrators]`,
  connects to the pinned peer and calls `orchestrator_port.ask_spawn` and
  `ask_join` (the spawn's window as given, a join's window plus 2 s). A name that
  is not configured is a definitive refusal. `daemon/main.reach_of` builds one per
  session, with `offered` reading `manager.unshared_sessions`;
  `serve.Settings.remote_reach` carries it to `agency.Config.remote`.
- `agency` admits `agent_spawn` with `on` through the holder (`AdmitRemote` ->
  `admit_remote`: the depth cap, `check_capacity` counting live stubs beside live
  cells, then `write_stub`), and runs the network on the caller's process
  (`ask_until_answered`: `poll.until_on` on the Agency's clock, doubling 50 ms to
  2 s, for `remote_window_ms` = 30 s; each attempt is told to wait only what is
  left of that window). A refusal is recorded as `Refused` (`record_refusal`) and
  answers `RemoteRefused`, unless the drainer was answered with a child first, and
  then the stub says `Spawned` and the model gets the child; the window running out
  answers `RemoteSilent` and leaves the stub `Requested`. Every exit that leaves the stub for the drainer, and every
  `Spawned` write, calls `config.remote_queued`. `is_live` is false for a cell with
  an `orchestrator`, and the stub carries the count. `context: my_conversation`, an
  unknown `on`, an unshared session and a background program are refused before
  anything is written. `wait` reads the stubs: a handle is remote when a stub holds
  it (the `@orchestrator` suffix is parsed and dropped), and a wait with any
  remote handle is `wait_across`, which runs the local wait and one `Join` per
  orchestrator as `weft` tasks. An unreachable join gives `Pending(unanswered:
  Some(orchestrator))`, a refused one `RemoteRefused`, and a `Ready` entry moves the
  stub to `Settled`. `roster` lists local cells and then `remote_peers` from the
  stubs, with a `Spawned` stub whose deadline has passed shown as `PastDeadline`.
  `send` to a remote child is refused by `refuse_remote_child`.
- `remote_drain.pass` is the second `peer_outbox_drain` instance's pass, in two
  steps over one read of the stubs, and the orchestrators the spawn step found
  silent are skipped by the join step. `spawn_pass` resends each `Requested` stub,
  skips an orchestrator that did not answer earlier in the same step, keeps a
  refusal it learns as `Refused`, reports `Owed` after it records a child as
  `Spawned` (the join step does not see that child until the next pass), and has
  no time to live. `join_pass` settles children nobody waits on: it groups the
  `Spawned` stubs whose deadline has not passed by (orchestrator, parent strand),
  sends one `Join` with a window of 0 for each group, and records every `Ready`
  with `remote_child.settled`. It stores no report, since a later `agent_wait`
  reads it again from the box. A pass is `Owed` while any child is running, so the
  drainer ticks every 5 s, and `Clear` when none is, so it goes idle; a stub past
  its deadline is not asked about. A refused join is logged and not retried within
  the pass; its slots are released by the deadline. `peer_outbox_drain.Options` is
  `{pass, after}`; `options` builds the peer-mail instance and `over` any other;
  `Backlog` is public. `serve` supervises both beside each other in the
  restartable service tier, rung by name (`outbox_queued`, `remote_queued`).

Not in S3: the box's deadline tick and retention sweep (S4), the shipped test and
the P model (S5), remote token metering (a child nobody waits on runs to its
`within_ms`, bounded by `max_within_s`), the stub's usage counting against a
goal's token budget or appearing in a status bar, releasing a child early (phase
2), and refusing `sessions.move` for a parent that holds a live stub.

Tests: `spawners_test` (a valid table, defaults, every refusal, the catalogue
parser), `orchestrators_test` (the `@` and `#` refusal),
`remote/orchestrator_port_test` (each message reaches the host with its origin,
refusals, a slow answer not delaying `Owns`, a crashing host),
`remote_spawn_test` (the pure half), `daemon_spawn_host_test` (the production
host's answers and what the Agency is told), `delegate_host_test` (the creation
for each placement, the cap, an invalid parent, a session that cannot open),
`daemon_manager_test` (the registry's half and `not_movable`) and `agency_test`
(a remote parent against a live Agency: adoption, the fence, run end, join with
usage, the cap on a join's window, the ceiling, the caps, and the whole path
through a real port). For the laptop's side, `agency_test` runs the parent against
`support/fake_orchestrators` (a table from call site to child, with `Up`, `Down`,
`LosesReplies`, `Refuses` and counted modes, and a record of the spawn and join
windows it was given): the stub written ahead, resends that leave one child, the
window clip, a refusal that keeps the stub `Refused` and a later answer that moves
it, the drainer adopting after a restart, a pass asking a silent orchestrator
once, a run end that sends nothing, an un-waited child settled by a pass, a stub
past its deadline freeing its slot while the box is down, the caps, mixed waits,
the roster and the refusals. `remote_child_test` is the codec and `is_live`,
`remote_reach_test` the empty reach, and `peer_outbox_drain_test` has an instance
over another pass.

## Explicit browser result reads

Protocol change 079 adds keyed result-page and result-download GETs. Each request
uses `server.page_grant` before and after its bounded read and resolves the entry
through the authorized resident's `result_reader`. `serve.Resident` holds only
that storage reader and its existing handles, never the runtime effect graph.
`ui_result` reads one fragment per viewer page or a complete bounded byte tree
under a shared five-second download deadline. No component callback or result
ledger is registered in `ui_sessions`.

## Offline LSP readiness and diagnostics scope

Automatic code-mode selection admits a workspace seed only after `seed.verify`
accepts it; an incomplete snapshot therefore falls through to the bundled
seed. An explicit seed flag remains authoritative and receives its own failure.
`lsp/manager.connect_jailed` resolves host Git before composing the downloader
and offline-server environments. `host_git.tool_path` changes subprocess lookup
without granting filesystem reads or network authority.

The manager owns one package server at a time. `diagnostics(None)` always returns
a partial `Unsettled` snapshot, while explicit file queries retain their own
server acquisition and settlement. A healthy control cannot certify unavailable
owners elsewhere in the workspace. Protocol 078 records that scope contract.

## Switching a session's profile (protocol 082)

`profile_get` and `profile_set` are session commands
([protocol 082](../../protocol-change/082-session-profile-switch.md)), answered
by a `profile` snapshot (`protocol.ProfileSnapshot`: `current`, `available`, and
`switched` only in the reply to a switch). The decoder reads an absent `profile`
as the default roles and refuses null, the empty string and a malformed name
(`optional_profile`). `ProfileSet` is owner-only (`gateway.owner_only` now
answers the refusal words) and is not a read.

The hub reaches the daemon through `profile_switch.Desk`, which
`serve.Settings.profile_desk` carries from the daemon's builder
(`daemon/main.profile_desk`) to `gateway.with_profile_desk`. A hub with no desk
answers `unsupported`. `gateway.set_profile` refuses while `State.live` is
non-empty, answers the listing for the profile the session already has, loads
the entering catalogue (`desk.load` → `daemon/profiles.load`), computes the
strands to move with `catalog.retargets` against `State.catalog`, saves the name
(`desk.save` → `manager.set_profile` → `catalogue.set_profile`), rewrites the
moved strands' models in one commit through `apply_changes` (the half of
`apply_config` after validation), replies, and calls `desk.restart`
(`daemon/restart.begin`: a `spawn_unlinked` stop and open that outlives the
session, because every weft start is linked to its caller). A strand follows a
role while its model is that role's head; a model chosen with `/model` heads no
role and stays. `default` is reserved: `catalog.default_profile`, refused as a
profile table name at load.

`Settings` gained `profile_desk`, so every `serve.Settings` literal in the tests
carries `profile_desk: None`.

## The session directory in Khepri (protocol-change/083)

A deployment may add a `[directory]` table, which makes the listed orchestrators
and executors the members of one Khepri cluster. Khepri then holds the single
authoritative record of which orchestrator owns each remote session (one created
with an executor or a pool), and every change of owner is one compare-and-set on
that record. Local sessions are recorded too, as lookup hints that never decide
anything. The catalogue rows stay as each daemon's local write-ahead memory.
A daemon without `[directory]` runs phases 3 and 5 exactly as the two sections
above describe. `docs/architecture/directory.md` is the design,
ADR-020 the choice of Khepri and its measurements, and
`protocol/models/session-move/KhepriMove.tla` the model.

- `internal/ffi_khepri` and `client_khepri_ffi.erl` are the only calls into
  Khepri 0.19.3 and Ra 3.2.0. The shim starts Loom's own Ra system
  (`loom_directory_ra`) and store (`loom_directory`) under
  `<state root>/directory`, normalizes every return to `{ok, _}`,
  `{error, {mismatch, Found}}`, `{error, no_quorum}`, `{error, not_running}`
  or `{error, {unexpected, Term}}` (an answer it does not recognise, printed),
  and catches exceptions at the boundary. The non-voter join is one primitive
  per Ra call (`join_start`, `join_remove`, `join_add`, `join_promoted`,
  `join_finish`), each answering `Done`, `Again` or `Failed(reason)`;
  `store.join` sequences them over `weft/poll` under one deadline.
- `directory/record` is `Record(owner, state)` with `OwnerState` `Serving |
  Local | Moving(op, to)`, stored as `{loom_owner, 1, Owner, State}` at
  `[loom, sessions, Id]`, and the marker `{loom_migrated, 1}` at
  `[loom, migrated, Node]`. Its decoder is total: node names go through
  `distribution.check_node_name` and the op through `catalogue.is_move_op`, and a
  payload that does not decode is an error, never an absent record. The payload
  types are opaque, so no `@external` leaves `internal/`.
- `directory/store` wraps the shim: `start_system`, `boot` (one member, creating
  the store when Ra knows no server: only bootstrap uses it), `restart` (the
  start of a member that has joined; it refuses when Ra has no record of the
  member's server, because `boot` would create a second one-member cluster),
  `join(node, within_ms, seat)` (`Seat` is `Voter | Learner`), `remove_member(name)`,
  `read` (local, `favor => low_latency`),
  `read_consistent` (`khepri:fence/2`, then a local read), `create`, `swap`,
  `delete_if`, `mark_migrated`, `membership`, `running_on(node)` and the `joined`
  marker file (`mark_joined` writes Ra's `names.dets` to disk first, since Ra
  flushes it on a 500 ms timer and the marker is what selects `restart`). Every
  call that can wait on a quorum runs in a weft run under a
  deadline (`write_ms` 3000, `activation_ms` 5000, `read_ms` 3000), because the
  consistent path ignores its own timeout without a majority. `WriteRefusal` is
  `NoQuorum(reason) | Mismatch(found)`.
- `directory/settings` reads `[directory] members` (refused beside a ledger,
  where `from_ledger` derives the members): three to seven distinct
  nodes, this node among them, every other one a pinned peer, and on a member
  every `[orchestrators.<name>]` node a member. An even count is accepted with a
  warning. `catalog` knows the key and `scripts/config_keys.sh` and
  `docs/configuration.md` document it.
- `distribution` takes `Cluster` (`NotMember | Member(members)`). A member
  starts with `hidden => false` and is refused unless the VM booted with
  `-kernel connect_all false` (`BootRefusal` `ConnectAllEnabled`); the launchers
  pass it with the TLS flags. `Peer.link` is `visible | hidden`, and
  `distribution.connect` uses `net_kernel:connect_node/1` only when both ends
  are members, so a link between members is visible whoever dials first.
- `directory/member` is the member's actor (`weft/actor`, a 2 s tick). It keeps a
  link to every configured member missing from `nodes()`, backing off by
  doubling to 30 s, and joins a member with no joined store through the first
  member whose store runs (`store.running_on`, then `store.join` under a 60 s
  deadline, then the marker). With a ledger its members are `Followed`, read
  from the ledger on every tick, and it also reconciles on the Ra leader (see
  "The membership ledger, the session directory" below). A store with the marker starts at `member.start`
  through `store.restart` and a refusal is a failed start whose message names
  the remedy (remove the marker and the member rejoins; for a one-member
  cluster, remove the directory and bootstrap again). `member.status`
  answers `directory.status`.
- `directory/ownership` is the record of write functions bound to this node:
  `create`, `begin_move`, `activate`, `abandon`, `release`, `record_local`,
  `release_local`, `mark_migrated`, `seed_moving`, the two reads and
  `migrated`. Only an orchestrator builds one (`main.ownership_of`: a daemon
  with a ledger is one when the ledger says its node is an orchestrator, because its
  rows change while it runs and the value is built once; a daemon without one decides
  from the executors, pools or orchestrators in its file), so an executor member never
  writes. `create` and `begin_move` answer a record already holding their
  value as committed. Tests bind two names over one shared book
  (`test/support/record_book`), because two registries in one VM share a node.
- `session_directory.khepri` is the lookup over the local copy, after the daemon's
  own catalogue: a tombstone there (`Moved(to)`) is `Elsewhere` its receiver
  without a read, as `peers` answers it, so a restarted member whose copy lags or
  is not joined still says where a session it handed away went. Otherwise
  absent is `Unknown`, this node `Here`, another node `Elsewhere` (the configured
  row, or a row carrying the node name), and an unreadable store `Unavailable(reason)`,
  which the server refuses `no_quorum` and peer mail treats as `Unreachable`.
  `as_member` adds the `ownership` and the `standing` (`Member(status)`) the
  server reads.
- `daemon/main` builds it all when `Config.directory` is set: `start_directory`
  starts the Ra system and the member actor (`Config.member`),
  `session_directory_of` chooses the Khepri lookup, and `start_movers` gives the
  movers `Recorded(ownership)` authority and a `session_movers.Upkeep`:
  `directory/migrate.seed` and `directory/deletion.finish_pending` on every
  tick, `migrate.cover_local` when local records are wanted, and
  `ownership.release_local` for a deleted local session.
- `daemon/manager` gains `reserve` (a registration reserved without opening it),
  `begin_delete`, `unmark_deleting`, `finish_delete`, `deleting_sessions`,
  `remote_registrations` and `local_sessions`. `prepare_slot` reads the deletion mark in the turn
  that reserves a slot and refuses `SessionDeleting` (code `busy`).
- `daemon/server`: a remote `sessions.create` on a member reserves, then
  `ownership.create`, then creates (`recorded`); `no_quorum` keeps the
  reservation. A remote `sessions.delete` goes through `directory/deletion.finish`:
  mark, conditional record delete, then the registration; `not_owner`, `moving`
  and `no_quorum` are its refusals. `directory.status` and `sessions.move` with
  `abandon: true` (`protocol.AbandonMove`) are member-only. `inbound_settled` is
  skipped on members. A local creation asks the movers for the local records
  (`Control.cover`) and does not wait; a local delete asks them to forget the
  record (`Control.forget`).
- `session_mover` takes `Authority` (`Rows | Recorded(ownership)`). Under
  `Recorded` it waits for this node's migration marker (`migrated`), writes the
  intent (`intended`: a record naming another owner carries on, an absent one is
  `Gone`, deleted elsewhere after the seed, and `gone` sets the copy aside as a
  retirement does, never reverting), retires only after a consistent read names another
  owner (`retire_recorded`), and abandons by the record first
  (`abandon_recorded`). An abandon carries its `Cause`: one that followed an
  answer and finds the record naming another owner retires; one that followed
  silence (`give_up`) returns `Deferred` and the next run asks the receiver
  again, because a receiver that crashed between its CAS and its import
  finishes only when asked (`taken_elsewhere`). `give_up` passes the same
  `migrated` check as a run. A stall a give-up could not cure (no quorum, not
  seeded, receiver already owns it) is `Deferred`, not `Stalled`.
  `session_movers` gives up a move after `give_up_after_ms` (30 min) of
  `Stalled` runs, and only once the receiver's marker exists;
  `Control.abandon` is the owner's hand-abandon.
- `session_importer.recorded` makes the activation compare-and-set before the
  import. A mismatch naming this node imports (a catalogue `Conflict` there is
  answered `Accepted`); any other owner or no record is `Refused(MoveEnded)` and
  drops only the incoming copy; `NoQuorum` is `Failed`.
- `directory/migrate.cover_local` writes `{self, local}` for every confirmed
  local session missing from this member's copy. The movers run it at boot and
  after `Control.cover`, and keep it wanted until a pass succeeds: a request
  count and the count the last successful pass covered, so a creation during a
  pass is covered by the next.
- `directory/migrate.seed` copies a version 13 catalogue's remote registrations
  into the store once: resident and imported become `serving` (or the activation
  for an imported session whose sender already wrote `moving`), moving becomes
  `moving` unless the receiver already owns it, moved writes nothing. A record
  naming another daemon is logged `directory.migration_conflict` and left. The
  marker is written last, once per node name, and nothing checks afterwards that
  every remote session has a record: a seeded member must not drop and re-add
  `[directory]` or change its node name, since sessions created in between have
  no record and a move of one would set it aside as deleted. To leave, move or
  delete its remote sessions first; to rename, decommission it and bootstrap or
  join it as a new member.
- `daemon/directory_cli` is `loomd directory bootstrap`: it refuses without
  `[directory]`, with a store on disk (the `joined` marker or a Ra server
  directory; the Ra system's own files do not count), while the daemon runs
  (it takes the endpoint reservation), and when a reachable member already
  runs a store. Each refusal names its remedy.

Invariants that break things when violated:

- A local transition that stops this daemon serving (`moving`, the deletion
  mark) is written in the registry turn before the record changes, and one that
  lets it serve (`imported`, the revert to `resident`) only after the record
  write that grants it committed. `KhepriMutantIntendBeforeStop` and
  `KhepriMutantImportBeforeCAS` are the model's checks.
- The receiver refuses a session its own row holds in any other state, `moving`
  included, before it writes the record (`activated` asks the catalogue first).
  Writing first can hand the record to a node that then cannot import, and the
  copy holding the newest version is dropped (`KhepriMutantImportOverMoving`).
- Every write expects a literal value; the activation and the abandon expect the
  same `moving` record, so exactly one commits. A blind write of either gives two
  owners.
- The source sets its file aside only on a consistent read naming another owner.
  A local read can be from before this daemon owned the session.
- A receiver never removes a session the record says it owns, and refuses only
  when the record names someone else: once it owns the record, an import that
  cannot finish is `Failed` and the copy stays (`owned_import`,
  `kept_if_owned`).
- No Khepri call runs inside a registry turn, and every one is bounded by a weft
  deadline. Opening a session makes no Khepri call.
- A local session's record is `Local`, never `Serving`, so no move or remote
  delete can use it as a compare-and-set base, and writing it never gates a
  creation. A stale one (a lost `forget`, or a delete from the web page, which
  does not ask) names the right owner, which answers `not_found`.
- A given-up move retires only on the receiver's answer
  (`KhepriMutantRetireOnSilence`).
- A missing record after the seed never grants serving: it means the owner
  deleted the session, and the source sets its copy aside
  (`KhepriMutantGoneReverts`).

Tests: `directory/record_test`, `directory/store_test`, `directory/ownership_test`,
`directory/settings_test`, `directory/migrate_test`, `directory/bootstrap_cli_test`
and `directory/daemon_record_test` (the store tests are serial: the store is a
VM-wide singleton); the `Recorded` cases of `session_mover_test`,
`session_importer_test` and `session_directory_test`; the
`members_visible`, `members_not_transitive`, `member_needs_connect_all_off` and
`directory_rejoin` scenarios of `distribution_test`; the directory cases of
`distribution_provision_test`; and the shipped member variants of
`daemon_shipped_remote_move_test`, `daemon_shipped_directory_test` and
`daemon_shipped_peer_mail_test`, with `daemon_shipped_directory_quorum_test`.

## Reading the deployment (protocol-change/085)

`deployment.status` is an owner-only control command that says which of the
nodes the daemon is configured with answer its own connection attempts, before
any session is placed on one. `docs/architecture/distributed.md` ("Reading the
deployment") is the account and `docs/client-protocol.md` §3.29 the wire.

- `client/deployment` is the whole read. `targets` lists the daemon's
  `[orchestrators]` rows and then its `[executors]` rows (a `Target` with its
  `Kind`), `only` narrows them to a configuration key, and `probe` connects to
  every target at once in one weft run (`connect_ms` 1000 each, `deadline_ms`
  3000 in all). `Reach(self, dial)` is how it connects: `over(membership, self)`
  is `distribution.peer` then `distribution.connect`, and `alone(self)`, for a
  daemon with no `[distribution]`, connects to nobody. A task that returns
  `Error(why)` is a `Failed` outcome carrying `why`, a task the deadline cut off
  is `Down(TimedOut)`, and a crash is `Down(Unavailable)`: a node nothing was
  heard from is never assumed up.
- The only names ever dialled are `Target.node` values from the daemon's own
  tables. The request's `node` is a key: `status` resolves it with `only` before
  `probe` runs, so a key the configuration lacks returns `Error(Nil)` and nothing
  was dialled. `protocol.node_field` holds it to the shape of a key first
  (`bad_request` otherwise). `daemon_deployment_test` and `deployment_test` assert
  the dial record is empty in both cases.
- `Health` is `Reachable | Down(Why)` with `Why` = `TimedOut | Unreachable |
  Unavailable` (wire words `timeout`, `unreachable`, `unavailable`). A node is
  `ok` or `down`; `degraded` is reserved on the wire and arrives with the node
  reports, which need a `Report` constructor on the orchestrator port and are not
  built. Every node member beyond `node`, `name`, `roles`, `state` and `reason`
  (when `down`) is optional so that report adds members and replaces none. The
  whole deployment's `state` is `deployment.verdict` (`Whole`, `Degraded`,
  `Lost`) over the configured nodes, not counting the asking daemon, which is
  always reachable: `ok` when all answered, `down` only when none did, `degraded`
  between. One dead executor among several is a degraded deployment.
- `Snapshot` is `Overview` (the asking daemon as a `Node`, `others`, `edges`,
  `placements`) or `Selected` (only the named nodes), so a reply with a `node`
  filter cannot carry edges or placements. Edges are `Serves` (executors) and
  `Peers` (orchestrators), each with the asking daemon's own `Link`. Placements
  are `ready | down` for an executor in this slice, and a pool lists
  `pools.candidates` in its trial order with `eligible` the reachable ones; the
  pool is never reordered by health.
- `server.Config.deployment` is the `Reach`; `main.deployment_reach` builds it
  from `Config.membership` and the node's own name. `server.deployment_status`
  adds `MemberRole` to the asking daemon when `directory.standing` is `Member`,
  and the owner check comes before any connection, so a member cannot make the
  daemon dial. The orchestrators come from `Control.orchestrators` (the movers'
  list, which is the `[orchestrators]` table on a daemon with distribution).

- The home page's creation panel reads the same placements without the rest of
  the deployment (protocol-change/085, `docs/architecture/web-view.md`,
  "Executors and pools"). `deployment.placed` connects to the executors alone,
  and `deployment.for_page` reduces the result to `web_view/placements` (a state,
  the operator's declaration, a pool's trial order and its reachable members; no
  node name or address). `server.HomeAttachment` gains `pools` (the startup
  capture of the `[pools]` names, like `executors`) and `placements`, the blocking
  read, which `ui_socket.upgrade_home` hands the page as
  `read_task(deliver, attachment.placements)` only where the page holds the
  creation capability. `ui_socket.create_for` accepts `creations.Pooled`: the pool
  and no executor reach `manager.Creation` (`pool_of`), the scope is session-only,
  and the daemon's `pool_unknown` is `creations.UnknownPool`.

Tests: `deployment_test` (the pure read and the weft fan-out with a scripted
`dial`, including a stuck connection cut by the deadline and a crash, and
`placed` and `for_page`),
`daemon_deployment_test` (the control socket: the owner's reply, a member's
`forbidden`, a key narrowing the read, an unconfigured key and a malformed one
dialling nobody), the `deployment.status` cases of `daemon_protocol_test`, and
`daemon_shipped_deployment_test` (two real daemons; the executor's process ends
and the next read says `down`). For the creation panel: `ui_route_test` (a pooled
creation and `pool_unknown`) and `ui_socket_test` (a card needs no new event
class).

## The membership ledger, pure part (protocol-change/086, slice 1a)

`client/membership_entry`, `client/membership_ledger` and
`client/membership_signing` are the format and the fold of the signed membership
ledger that will replace the fixed `[[distribution.peers]]` list.
`docs/design-notes/membership-ledger.md` is the argument and
`protocol-change/086-membership-ledger.md` the normative format. This slice has
no file, no process, no `loom.toml` key and no wire message: nothing reads these
modules yet except their tests and the provisioning code below, and static pins
are unchanged.

- `membership_entry` is the bytes. An `Entry` is `seq`, `prev` (an `EntryId`),
  `time` and a `Kind`: `Genesis(deployment, authority)`, `Upsert(Member)` (wire
  word `member`) or `Revoke(name, pin, reason)`. `encode_body` writes the JSON with
  the keys of the format in order; `read` is steps 1 through 3 of the acceptance
  procedure (`parse_line`, then the injected verifier over `signing_input`, then
  `decode_body`) and never parses a body whose signature failed. A reader does not
  re-serialize: a `SignedEntry` keeps the exact `body` bytes, and its `id` is the
  SHA-256 of them. `parse_line` also refuses a base64 half that is not the
  canonical encoding of its bytes, which 086 states: without it a flipped last
  character would still decode to the same bytes. `seal` is the
  signer's side and does not validate the fields; `membership_ledger.check` does.
- `Signer` and `Verifier` are function values, so the module has no `@external`
  and a test needs no key. `Verdict` is `Authentic | Forged`. `membership_signing`
  builds the real pair from the authority PEMs through two functions added to
  `client/internal/ffi_pki` (`sign`, `verify`), and `authority_pin` is the value a
  genesis names.
- `membership_ledger` is the fold. A `Ledger` is opaque and immutable: the active
  members by name, `ever` (every name enrolled), the head, and the identity applied
  at each sequence number. `begin` and `start` take the genesis, `accept` runs the
  whole procedure for one line, `apply` runs steps 4 through 7 for an entry already
  read, and `replay` is `begin` followed by `accept` over a file. A refusal returns
  `Error(Refusal)` and the caller keeps the ledger it had, which is how a fork
  leaves a node unchanged. A duplicate and a gap are `Outcome`s and not refusals.
  `Refusal` is `Unreadable(ReadError)`, `Fork(seq, ours, theirs)`, `Chain` or
  `Invalid(Rule)`, and `step` gives the control protocol's word for each.
- The fold's rules are `Rule`: no node or pin shared with another active member,
  links only to other active members, at most 64 active and 256 names ever, at
  most 10,000 entries (`max_entries`), a revoke only of an active member, and a
  genesis only first. A revoke also removes the revoked name from every remaining
  member's `links`, which 086's fold states: a name enrolled again later must not
  get its old edges back.
- `pin_table(ledger, name)` is the table for one node: each active member other
  than `name` that is linked to it (either lists the other, or both have a
  directory standing other than `none`). `lookup_pin` is what the verify callback
  will read and `lookup_node` its inverse. A name that is not active has an empty
  table and `enrollment` says whether it is `Revoked` or `NotEnrolled`. The caller
  does the effects of steps 8 through 10 (append, publish, disconnect), in that
  order.
- `check(ledger, entry)` is the signer's pre-flight: the entry must decode to
  itself, follow the head and satisfy the fold.
- Tests: `membership_entry_test` (golden body strings, every malformed shape),
  `membership_ledger_test` (each refusal, the bounds, the pin table),
  `membership_ledger_property_test` (replay equals stepwise apply, prefixes,
  flipped bits, swapped and removed entries, forks, equal tables, the pin table
  against a model, symmetric links, no pin of a revoked name, totality over
  arbitrary bytes) over `support/membership_fixture` (an HMAC stand-in for the
  signature, so the laws need no key) and `support/membership_log` (a log builder
  and a seeded generator that keeps the random operations the real fold accepts),
  and `membership_signing_test` (the real ECDSA pair).

## The membership ledger, file and process (protocol-change/086, slice 1b)

`client/membership_store`, `client/ledger_service` and `client/ledger_boot` run
the pure ledger above on one node and replicate it between nodes. Nothing here
changes who may connect by itself: slice 1c below makes the ledger the trust list
through an observer.

- `membership_store` is the file: one signed line per entry, each followed by
  `\n`. `open(path, verifier)` reads it (at most 64 MiB), drops a final line with
  no `\n` (a write that did not finish, valid-looking or not) and cuts it from the
  file, then replays the rest through `membership_ledger.replay`. A line the fold
  refuses stops the open with `Corrupt(line, refusal)`, one-based; a missing or
  non-text file is `Unreadable`, an empty one `NoGenesis`. `append` rewrites the
  file beside the old one, fsyncs and renames it (`bootstrap.atomic_write_private`,
  mode 0600), so a crash leaves the old file or the new one. That is O(file) per
  entry, at most 10,000 entries, and it needs no new Erlang. `lines_after` serves
  a pull within 64 lines and 256 KiB.
- `ledger_service` is the `loom_ledger` process, a `weft/state_machine` with the
  phases `Quiet` and `Pulling(source, via)`. Its fixed registered name is
  `ledger_service.default_name()`. Wire messages (closed, no catch-all):
  `Head(seq, id, from: Subject(Message))` and `Pull(after, limit, reply:
  Subject(Entries))`, answered by `Entries(lines, head_seq)`. `Head` carries the
  sender's inbox because a message to a registered name does not say who sent
  it; 086 now says so. Local calls: `offer(service, line, within)` returns
  `Accepted(head) | AlreadyHeld | Behind(head) | Refused(refusal) |
  NotDurable(reason)`, `head`, `entries(after, limit)` (the local twin of `Pull`),
  `status` (ledger, last refusal, reported heads with `ms_since`), `announce`
  (send heads now) and `meet(reach)` (a peer just connected).
- Order, per line: `membership_entry.read`, `membership_ledger.apply`, then
  `membership_store.append`, and only then the observers and the in-memory
  ledger move. A file that cannot take a valid line leaves the node unchanged
  (`NotDurable`). `Observer`s are called inside the process, synchronously,
  with `Loaded(ledger)` before `start` returns, `Extended(previous, ledger, entry)`
  after each durable append and `Connected(node, ledger)` on a node-up; slice 1c
  publishes the pin table and disconnects from them.
- Anti-entropy: `Start` sends a head to every connected peer and arms the one
  periodic `Tick` (60 s, `tick_ms`); each tick and each node-up sends a head. A
  head ahead of ours is pulled from in batches of at most 64 under a 5 s state
  timeout; a head at our sequence number with another identity is pulled for that
  one entry so the fork becomes a recorded refusal; a head that would start a
  second pull is postponed until `Quiet`. Entries are applied whoever sent them.
  The next batch is asked for only if the head moved, which ends a peer that
  answers with old or invalid lines. After applying, a node sends its head to
  every peer but the source. A refusal is recorded as the last one (replaced by
  the next) and never sticks. Peers are `Distributed` (the VM's connected nodes
  and `net_kernel:monitor_nodes`, through `internal/ffi_nodes`, stock OTP
  bindings with no Erlang source) or `Supplied(reach)` for tests. Dialing is
  slice 1c.
- `ledger_boot` is the daemon's glue; slice 1c changed it, see below.
- Tests: `membership_store_test`; `ledger_service_test` (two to three services in
  one VM over real files with `support/ledger_rig`: outcomes, refusals,
  durability, tick, batches, relay, garbage, deadline, postponed heads, fork,
  observers); `ledger_boot_test` (real ECDSA, serial because it registers
  `loom_ledger`); `ledger_distribution_test`, a three-emulator chain over the
  production TLS flags (`client_distribution_fixture_ffi` scenario
  `ledger_relay`, node side in `support/ledger_nodes`) where a revoke reaches a
  node the origin never connects to.

## The membership ledger, provisioning (protocol-change/086, slice 1d-i)

`provision` signs a ledger for the plan and keeps the authority key, unless
`--static`. Static mode, `[[distribution.peers]]` and `/1` bundles are unchanged.

- `client/authority_key` is custody of the key. `keep` writes
  `<home>/.loom/authority/<deployment id>` (directory 0700) holding `key.enc`,
  `ca.pem` and the operator's `ledger.log` (files 0600), key last, and refuses
  before writing anything when `key.enc` exists: a key is never replaced.
  `key.enc` is one JSON line, format `loom-authority-key/1`, keys exactly
  `deployment`, `iterations`, `salt`, `nonce`, `ciphertext`, `tag` (unpadded
  URL-safe base64, canonical encodings only). The key is PBKDF2-HMAC-SHA256 at
  600,000 iterations (the floor a reader enforces; a ceiling of ten million
  bounds a hostile file) over a random 16-byte salt, the cipher AES-256-GCM with
  a random 12-byte nonce, and the associated data is the format and the
  deployment id. A fresh salt per file means one message per key, so a random
  nonce cannot repeat. An open that fails is `Rejected`: GCM cannot tell a wrong
  passphrase from a changed file, and the module does not claim to.
  `signer(directory, deployment, passphrase)` is the one-function seam; a
  keychain or token replaces it and nothing that holds a `Signer` changes.
- A passphrase is 12 to 1024 bytes, from `passphrase_from_terminal` (the VM's
  raw `noshell` mode, which echoes nothing; `NoTerminal` when stdin is not a
  terminal or `TERM` is unusable) or `passphrase_from_file`, which goes through
  `bootstrap.read_private_bounded` and so refuses a file others can read. Never
  an argument. Nothing in the tests calls the terminal reader, because a test
  run from a terminal would block on it; it was driven under a pty by hand.
- `client/membership_provision.build` writes the genesis and one `member` entry
  per plan node in plan order. `links` are the plan peers already enrolled
  (the fold accepts a link only to an active member), one end per edge, which the
  pin table treats as symmetric; plan directory members are `voter`, everyone
  else `none`. Each entry passes `membership_ledger.check` before it is signed,
  and the finished file is replayed with the authority certificate's verifier.
  `distribution_provision.peer_pins` is the static peer list, and
  `ledger_provision_test` holds every node's ledger pin table equal to it.
- Bundle `/2` replaces `peers` with `ledger` (the lines, in order). `Trust` is
  `StaticPeers | SignedLedger`, so a bundle with both cannot be built, and a
  document with `peers` under `/2` or `ledger` under `/1` is refused for the
  unknown key. `decode` replays the ledger with a verifier from the bundle's `ca`
  and requires the genesis to name that CA and the node's own entry to be active
  with the bundle's role, node name and the pin of its certificate. The peers
  that `executors` rows and `directory` are checked against are the node's pin
  table. `config_tables` writes `ledger = "<dir of ca>/ledger.log"` and no peer
  row (`ledger_path`, `ledger_file`).
- `install` takes a `/2` bundle (slice 1d-ii, below).
- `provision` writes a ledger by default and `--static` is the opt-in for the
  `/1` bundles. It checks the output directory and reads the passphrase before it
  mints, seals the key before it writes the bundles, and writes `OUT/ledger.log`
  beside them. `Provisioned` carries the ledger text as a `String` for
  `keep_authority`; `Deployment.ledger` stays an `Option` because `write` is
  shared with static mode. A plan with a session directory is provisioned in
  ledger mode too (phase 3: its members are voters). `--static` is refused beside `--drop-authority`,
  `--passphrase-file` and `--home`, which only a ledger reads.
- The terminal prompt works only under `loomd distribution`. `loom
  distribution …` runs `loomd` through a port whose stdin is a pipe, so the
  prompt would answer `NoTerminal`; the launcher (`tui.forward_distribution`)
  refuses `provision` with none of `--passphrase-file`, `--drop-authority` and
  `--static`, and `enroll`, `revoke` and `renew` without `--passphrase-file`,
  before it spawns, and the usage text says so.

- The kept directory has a fourth file, `cookie` (`Kept.cookie`,
  `authority_key.cookie`, 0600, unsealed): `enroll` and `renew` write bundles and
  a bundle carries the cookie. `provision_ledger`'s `Authority` carries it.

Tests: `authority_key_test` (round trip, wrong passphrase, each changed field,
the deployment binding, the shape and weak-parameter refusals, permissions,
overwrite refusal, the signer) and `ledger_provision_test` (the equivalence with
static pins for a plain and a directory plan, standing defaults, the `/2` bundle
refusals, the config tables, the install refusal, the 65-member plan, and the
commands).

## The membership ledger, TLS and dialing (protocol-change/086, slice 1c)

Ledger mode makes the ledger the only source of trust: `[distribution] ledger`
replaces `[[distribution.peers]]`, and a document with both is refused by
`distribution.from_document`.

- `distribution.Config` holds a private `Trust`, `Pinned(peers)` or
  `Ledgered(path)`. `configure_ledger` builds the second; `tls_options` renders
  the pin-less file (`ffi_distribution.live_options`) and `start` calls
  `ffi_distribution.start_live` (the same Erlang functions as the static ones,
  which accept the atom `live` in place of a peer list). `peer_nodes` is empty in
  ledger mode, which only `client/ledger_reference.peers` reads: it answers
  `Ledgered`, so the tables that name a peer check shape only (slice 1d-ii);
  `[directory]` is refused beside a ledger (the members are the ledger's, see the
  phase 3 section). `distribution` and `distribution_plan` must never import a `membership_*` or `ledger_*`
  module (`membership_entry` imports both), which is why the mode is a type in
  `distribution` and the ledger side starts it.
- `ledger_trust` is the observer that applies the ledger. It runs inside the
  ledger process, which therefore owns the protected table `loom_dist_pins`:
  `publish` (FFI `publish_pins`) creates it on the first call and replaces the
  `pins` row with one insert, after the durable append. Order per `Extended`:
  publish, `net_kernel:allow/1` (grows, never shrinks, `ffi_nodes.allow` is the
  one place a ledger name becomes an atom), then disconnect `stale` nodes, which
  are the connected nodes the new table does not list or whose pin the entry
  replaced. `Connected(node, ledger)` disconnects a node that is not in the table
  of the ledger the process holds at that moment. A self-revoke leaves the empty
  table (every handshake refused, every connection dropped) and keeps
  `net_kernel` up.
- Bounds: the table never holds more than `membership_ledger.max_active - 1`
  pins, and the allow list never grows past `max_names` (256) names, both
  enforced by the fold. `ledger_trust.max_peers` states the first, and `publish`
  never publishes more.
- `ledger_boot.start(config, cluster, logger)` returns `Booted(membership,
  ledger)`. With a ledger it runs `open_service` (verify the file, require this
  node to be an active member, start the process with the observers), then
  `distribution.start`, then `begin_trust` (allow list, `enable_dialing`). The
  ledger starts first so the table exists before the listener does. `start_with`
  takes the observers, for the test that holds the process. A `Member` cluster is
  refused. `daemon/main.Config.ledger` keeps the process handle.
- Dialing (`ledger_service.enable_dialing`, `redial`): a periodic pass attempts
  each wanted node that is not connected and has not reported the current head,
  with waits of 5 s doubling to 5 min kept per node; a node that stops being
  pending loses its backoff. `wanted` is `ledger_trust.executors_to_dial` (an
  orchestrator's linked executors) and the attempt is a spawned
  `distribution.peer` and `distribution.connect`, so the process never waits on
  a handshake. In live mode `peer` resolves a name only if the table lists it.
- Tests: `ledger_trust_test` (stale and dial decisions), `ledger_service_test`
  (fake-clock backoff, observer arguments), `ledger_boot_test`, and
  `ledger_live_test` over `client_ledger_live_fixture_ffi` and
  `support/ledger_live` (emulators, production PKI): links-only connections and
  dialing, enrollment without a restart, revoke dropping connections, and the
  node-up check alone ending a handshake that read the old row (a hook holds the
  ledger process before the publish; removing the check times the test out).

## The membership ledger, install and the operator commands (protocol-change/086, slice 1d-ii)

`install` takes a `/2` bundle, `provision` writes one by default, and
`enroll`, `revoke`, `renew`, `log` and `import` extend and read the ledger.

- `distribution_install` writes the ledger beside the credentials
  (`distribution_bundle.ledger_path`), merges a `[distribution]` table with a
  `ledger` key and no peer rows, and renders the live options file. The ledger
  file is planned as a chain (`plan_ledger`): a file the bundle extends is
  replaced, a file that extends the bundle is kept, two that diverge are refused
  unless `--force`. A `/2` bundle lists no directory (`decode` refuses one). The bundle's executor rows are not read against the ledger at install
  (it is not on disk yet); `decode` already required each to name a peer in the
  node's pin table. `Installed.snapshot` is the entry the bundle ends at.
- `client/ledger_reference` opens a node's ledger from its `[distribution]` table
  (`load`, with the checks `ledger_boot.open_service` used to make). It answers
  `peers(config)`: `Pinned(nodes)` in static mode and `Ledgered` in ledger mode,
  and `executors`, `orchestrators`, `spawners` and `directory/settings` test their
  `node` with `accepts`. So parsing a table never reads the ledger; a session
  parses them on every create and open, and a `revoke` cannot fail one. The check
  against the ledger is `audit(ledger, local, path, rows)`, which `daemon/main`
  calls once at start (`audit_references`, over the ledger process's own state,
  rows from `reference_rows`): a node no member entry ever named
  (`membership_ledger.ever_named`) is an `Error`; a node that was a member and is
  not an active linked member is an `Inert` row, logged as
  `daemon.ledger_row_inert`. A call through an inert row fails because
  `distribution.peer` resolves only names in the pin table.
- `client/ledger_control` is both halves of `ledger.append`, `ledger.entries` and
  `ledger.status`. The daemon half takes a `Handle` (the `loom_ledger` process,
  the node's name and its plan name from the ledger it started with, since a
  revoked node cannot find itself by node name) and answers JSON; a refused
  line is `Invalid(step)`, which `daemon/server` serves as `ledger_invalid`
  with the step as the message (`gap` for a line past the head). The operator half
  is total decoders (`decode_status`, `decode_entries`, `decode_applied`) and
  `pages`/`fit_page`, which bound a frame to 48,000 bytes of lines. `daemon/main`
  builds the handle after `start_distribution` and `server.Config.ledger` carries
  it; a daemon without one answers `not_found`. `host/access` carries the
  `ledger_invalid` message to the caller (`refusal_code`) and has
  `raw_remote_request`; `daemon/admin.ledger_request` takes a `Where`
  (`ThisHost` or `OverNetwork`) and validates with the daemon's decoder.
- `client/ledger_operator` is the pure part of the commands: `enroll`, `renew`
  and `revoke` build the entry from the fold (a renewal copies the member's
  current fields from the fold, not its last entry) and run
  `membership_ledger.check`, `describe` turns a refusal into a sentence,
  `concerns(before, after)` lists what a revoke newly breaks (`Orphaned` executor,
  `Cut` members), and `confirmations(status, seq)` says who holds an entry.
  `client/ledger_sync` keeps the operator's copy and a daemon in step: `catch_up`
  pulls what the daemon has that the copy lacks, verified through the fold, and
  refuses a fork, `hand_over` sends the daemon every line after its head, and
  `await` polls `ledger.status` (weft poll) for at most `--wait`, not waiting for
  the member just enrolled or renewed.
- `daemon/ledger_cli` runs the commands. A signing command unlocks the kept key,
  locks `<authority dir>/ledger.lock` (`bootstrap.try_launch_lock`), asks the
  daemon for its status (an unreachable daemon is a reason and the entry stays in
  the copy), catches up, decides, seals, applies, appends to the copy, writes the
  bundle (`enroll`, `renew`) after checking it decodes, hands over, and prints the
  confirmations. `run_with` takes the way to reach a daemon (`Connect`) so a test
  uses a ledger process in its VM; `run` uses the socket. `revoke` does not check
  that the member owns sessions on its own: it asks the daemon (phase 2, below),
  and it counts the directory's voters (phase 3). `enroll` takes
  `--directory voter|learner|none` (phase 3), and refuses an executor with no
  `--links` (`ledger_operator.check_enrollment`: it would be linked to no
  orchestrator, and links cannot be added to a member without a revoke). `renew`
  takes `--bundle-dir` like `enroll`, since the ledger does not record where a
  node was installed. A bundle that cannot be written leaves the entry in the
  copy, and the error says to run `renew NAME` (`or_bundle_not_written`).
- `ledger_service` sends the puller's head to the peer it pulled from, so the
  source's `status` reports it within seconds (without it, at the next tick).
- Tests: `ledger_install_test` (install, idempotence, fork refusal, boot from
  the install, the remedy for a node that applied its own revoke),
  `ledger_reference_test`, `ledger_control_test`, `daemon_ledger_test` (the
  socket, bounds, `not_found`), `ledger_operator_test` (pure),
  `ledger_cli_test` (each command with a ledger process as the daemon: offline,
  online, refusals, catch-up, fork, import, log), `ledger_operator_live_test`
  (provision, install, three emulators over TLS, enroll while they run, revoke,
  log; the operator's requests are relayed to the node's `ledger_control`), over
  `support/ledger_deployment`, `client_ledger_operator_fixture_ffi.erl` and the
  `control` command of `client_ledger_live_fixture_ffi.erl`.

## The membership ledger, configuration and status (protocol-change/086, phase 2)

An orchestrator in ledger mode gets its `[executors.<name>]` and
`[orchestrators.<name>]` rows from the ledger as well as from its file, `revoke`
checks what it would strand, and `deployment.status` shows the ledger. The design
note's "Phase 2: configuration and status, what was decided in the build" is the
argument.

- `client/roster` is `Roster(row)`, an opaque function that returns rows when it is
  read (`fixed`, `reading`, `rows`). `session_importer.Context`,
  `session_mover.Environment`, `session_movers.Control`, `server.Config.executors`,
  `session_directory.peers`, `khepri` and `settle_over` and
  `remote_spawner.over_distribution` hold a `Roster` where they held a list, and call
  `roster.rows` at the moment they decide. A holder never keeps the list it read
  across requests. `daemon/main` builds the two rosters from `Config.executors` and
  `Config.orchestrators` (the written rows) and `Config.ledger`
  (`executor_roster`, `orchestrator_roster`): a daemon with no ledger wraps the
  written lists in `roster.fixed`. A per-session assembly (`workspace.fixed` and
  `pooled`) reads the roster once for each open. `Config.executors` and
  `Config.orchestrators` stay lists of the written rows.
- `client/ledger_rows` derives the rows. `executors_of` and `orchestrators_of` take a
  ledger, the node's own node name and the written rows. Only an active orchestrator
  derives anything: the rows are the members of its pin table by role, named as the
  ledger names them, `executors.plain` and `orchestrators.plain` (no declaration, no
  address). An orchestrator with a directory standing derives only orchestrators that
  have one. A written row claims its name and its node, a derived row it claims is
  dropped, and the result is sorted by name (`combine`). `executor_roster` and
  `orchestrator_roster` read `ledger_service.status` each time and fall back to the
  written rows when the process does not answer in two seconds. Never call a roster
  from an observer: observers run inside the ledger process.
- `ledger_reference` gains `NameRow`, `InertName` and `audit_names` for the executor
  names that pools and spawners list. `pools.from_document` and the spawner's
  placement check a name's grammar only, in ledger mode (`ledger_reference.Ledgered`),
  and `daemon/main.audit_started` calls `audit_executor_names` after the node audit
  with the names of the written executor rows: a name nobody gave or an orchestrator's
  name is an `Error`, a revoked or unlinked executor is `daemon.ledger_name_inert`.
  `reference_names` lists the references with their keys.
- `Holds` is the orchestrator port's new constructor: `Holds(reply: Subject(Result(Held,
  Nil)))`, answered by a `Holder` that `start_with` takes as its last parameter and
  runs through `detached`. `Held(sessions, placed)` counts the active registrations
  whose custody is not `Moved`, and `Placed(executor_node, sessions)` the ones placed on
  each executor the asker lists (at most `max_placed`, 64). `ask_holds` flattens silence
  and a catalogue that could not be read into `Error(Nil)`. `unheld` is the holder of a
  port with no catalogue. `client/ledger_holdings` holds the daemon side: `targets`
  (the node itself if it is an active orchestrator, and the orchestrators in its pin
  table), `collect` (one weft run under `deadline_ms`, 4 s, inside the 5 s the control client
  waits for any reply; each ask is silence or a `Held`), `over_distribution` (its own port for the local node, `session_directory.reached`
  for a peer, then `ask_holds`), `silent`, and `from_catalogue` (`from_pages` takes the bound), which walks
  `manager.page` for at most `max_pages` pages and answers unknown, not partial, when
  the next page is not empty or when more than `max_placed` executors hold sessions,
  drops `Moved` custody (one
  `manager.custodies` call a page) and maps the executor names in the catalogue to nodes
  through the executor roster. `ledger_control.Handle` has an `asker` (`handle` gives
  `ledger_holdings.silent`, `asking` replaces it; `daemon/main.ledger_handle` gives it
  `over_distribution`), and `ledger_control.holdings` is `ledger.holdings`
  (`protocol.LedgerHoldings`, served by `server.ledger_command`).
  `ledger_control.decode_holdings` is the operator's total decoder
  (`AnswerView`, `HeldView`, `PlacedView`), bounded to 64 orchestrators and 64 placements
  and refusing a negative count.
- `ledger_operator.strandings(ledger, name, answers)` is the decision, with `Stranding`
  = `Owns(n)` | `Placed(n, by)` | `Silent(names)` | `Unasked(reason)`.
  An orchestrator is `Owns` when its own answer reports sessions and `Silent` when it
  has none. An executor is `Placed` by the orchestrators linked to it that put
  sessions on its node, and `Silent` for each of those that did not answer.
  `describe_stranding` and `describe_clear` are the sentences. `ledger_sync.Daemon` gained
  `holdings`. `daemon/ledger_cli.revoke` runs `checked_revoke` before it signs (unless
  `--force`): the link graph, the strandings and (phase 3) `ledger_operator.check_voters`
  together, one refusal listing all that fail. A
  daemon that cannot be asked is `Unasked`, so an offline `revoke` now needs `--force`.
  `sign` returns the decision's extra value beside the `Signed`, which is how the
  sentence about what was checked reaches the output.
- `server.deployment_status` adds `ledger` (the body of `ledger_control.status`) to a
  whole-deployment read on a daemon with a ledger handle; a read naming a node, a daemon
  with none and a ledger that does not answer add nothing. The terminal reads it in
  `tui/deployment_view`.
- Tests: `ledger_rows_test` (derivation, combination, a roster that follows a running
  ledger process, fail-closed), `ledger_reference_test` (the name audit and the
  ledger-mode pool and spawner parse), `ledger_holdings_test` (targets, the fan-out, a
  real registry for `from_catalogue`: counts, placements, two names for one node, moved
  and moving custody, a page boundary, an unreadable registry), `orchestrator_port_test`
  (`Holds`), `ledger_control_test` and `daemon_ledger_test` (the command and its
  decoder over the socket, `deployment.status` with and without a ledger),
  `ledger_operator_test` (`strandings`), `ledger_cli_test` (owns, silent, placed,
  unasked, both refusals together) and `ledger_operator_live_test` (three emulators:
  enroll and revoke while they run, and the rows follow). The live fixture nodes run no
  catalogue, so their `ledger.holdings` answers that every orchestrator holds nothing,
  unless a scenario sends `serve_holdings N EXECUTOR_NODE`, which starts a port over a
  real catalogue (`support/held_catalogue`) and makes that node ask over distribution
  (`ledger_live.Holdings`): `ledger_operator_live_test`'s holdings scenario is the
  whole path across two emulators. `daemon_ledger_test` runs the revoke of a silent
  orchestrator over the real control socket, and `test.rows` is a fixture-only command
  that lists the derived rows.

## The membership ledger, the session directory (protocol-change/086, phase 3)

With a ledger the session directory's members are the ledger's, and the Khepri
cluster follows them. `docs/architecture/directory.md` ("Following the membership
ledger") is the account and the design note's "Phase 3: what was decided in the
build" the rulings.

- `directory/seats` is the pure part (not `client/roster`, which is phase 2's rows read
  when used). `places(ledger)` reads the active members
  whose `directory` standing is `voter` or `learner` as `Place(node, seat)`, ordered
  by node, and `read(ledger)` gives `Seats`, which add `named`, whether any entry has named a node.
  `plan(seats, ra, local)` returns a `Verdict`: `NotLeader` (this member is not
  the Ra leader), `NotSeated` (it leads and the ledger gives it no seat),
  `Remove(node, Why)` (`Unseated` or `OtherSeat`), `Held(node, Why)` (a voter whose
  removal could not commit, because a majority of the voters that remain is not in
  `ra.up`, the members the leader's `members_info` reports with status `normal`), or
  `Settled(missing, foreign)`. `rejoin(seats, local, answer)` is the other decision:
  `Rejoin` only when the ledger seats the node and a leader's membership, asked
  through a peer, answers without it; `Stay` for a timeout, no leader and an
  unseated node. Learners are removed before voters. A Ra
  member the ledger has never named is `foreign` and stays; a seat nobody has taken
  is `missing` and the node joins by itself. The plan never adds: only the joining
  node can start the Ra server with the UId it is recorded under.
- `store.Seat` is `Voter | Learner`. `store.join(node, within_ms, seat)` is the four
  steps with the seat's membership: `promotable` and a wait for the promotion for a
  voter, `non_voter` and `join_caught_up` for a learner (the server has heard from
  the leader, lists itself in its configuration, and has applied what the cluster
  had applied). `ffi_khepri.Voting` gains `Promoting` so a voter that is catching up
  is not read as a learner. `store.remove_member(name)` is `ra:remove_member` through
  this node's own server; `client_khepri_ffi:remove_member/2` looks the name up as an
  existing atom and makes none. `store.membership_through(node)` is the same
  `ra:members_info` addressed to a peer's server, which forwards it to the leader;
  `store.Membership` carries `up`. `store.reset(directory)` removes the joined marker
  first and then deletes the local server, so a stop between them leaves a node that
  has not joined and not a marker over no server. `store.settling` is the text of the
  refusal for a change Ra will not take until the last commits.
- `member.Members` is `Listed(nodes)` (every node a voter, no reconciliation) or
  `Followed(read)` (the ledger's `Seats`, read each tick; an unreadable ledger seats
  no one, and seats that seat no one removes no one because `named` is false).
  A tick keeps links and tries the join for the seats' places, and with `Followed`
  and a joined store runs `reconcile_once` in a task. A seated node whose own
  `store.membership()` fails or does not list it asks the seated peers that are
  connected (`first_answer`), and resets (`Reset`, then `Joining` is `Rejoining` and
  the next tick joins in the ledger's seat) only on a leader's answer without it;
  otherwise the pass is `seats.plan` and a `Remove` is applied through
  `store.remove_member`. A refusal because a change is settling is logged once as
  `daemon.directory_remove_held` and not as a failure every tick. The decision is
  logged when it changes: `daemon.directory_member_removed`,
  `daemon.directory_remove_held`, `daemon.directory_remove_failed`,
  `daemon.directory_reconciled`, `daemon.directory_not_a_member`,
  `daemon.directory_reset_failed`. `directory.status` reports `joined: false` and
  `unavailable` while a member rejoins.
- `directory/settings`: a `[directory]` table beside a ledger is refused;
  `from_ledger(ledger, local)` is `Some(Settings(members))` when the ledger seats the
  node; `require_orchestrators` is the `[orchestrators]` rule, called with the
  ledger's members. `daemon/main.ledger_directory` derives the daemon's settings from
  the ledger process at start and logs `daemon.directory_voters` for a voter count
  outside three to seven or an even one. `main.members_of` gives the member actor
  `Followed` when `Config.ledger` is set.
- `ledger_boot` reads the node's standing from the ledger and starts distribution as
  `Member([])` when it is seated (visible, `connect_all` off) and `NotMember`
  otherwise; it refuses a `Member` cluster passed with a ledger.
  `ledger_trust.publish` publishes `directory_nodes` (the linked members whose
  standing is not `none`, empty on a node outside the directory) as the `directory`
  row of `loom_dist_pins` (`ffi_distribution.publish_directory`), and
  `client_distribution_ffi:link/2` reads it in live mode on every connect.
- `daemon/directory_cli`: `bootstrap` takes the members from the ledger when
  `[distribution]` names one (`start_ledgered`: `ledger_boot.start`, then requires a
  voter seat) and from `[directory]` otherwise (`listed_members`, read before
  anything is reserved).
- Operator: `Enrollment.directory` carries `--directory`; `ledger_operator.check_voters`
  refuses an `enroll` that makes the eighth voter and a `revoke` that leaves fewer than
  three (`revoke --force` skips it). Provisioning writes a ledger bundle's `directory`
  as `[]` and `decode` refuses a ledger bundle that lists one; `install` no longer
  refuses a directory plan.
- Invariants: the leader never adds, never removes itself, and removes a voter only
  while a majority of the remaining voters is replicating its log (Ra status
  `normal`); a Ra member the ledger has not named is never removed; a learner that has
  just joined learns the leader from the leader's next append (the fixture writes
  before it reads the learner's view); a removed member resets only on a leader's
  explicit answer, only when the ledger seats it, and never creates a store (the
  marker goes before the server, and a join is the way back).
- Tests: `directory/seats_test` (the plan as values), `ledger_spike_non_voter_test`
  (the production learner join and `remove_member` over two scenarios in
  `client_ledger_spike_ra_fixture_ffi`), `ledger_operator_directory_test` and the
  directory cases of `ledger_cli_test` (the voter bounds), `settings_test`,
  `ledger_trust_test` and `bootstrap_cli_test`, `daemon_ledger_directory_test`
  (`main.ledger_directory`), `ledger_provision_test` (a directory plan with a
  ledger), and `ledger_directory_test`, which provisions, installs and boots three
  voters over TLS with the production member actor, enrolls a learner while they run,
  sees a revoke refused, ends the leader and watches the new leader remove the revoked
  learner (the store runs in emulators of its own, so the test VM needs no serial
  entry; it takes about 35 s), plus a scenario that revokes a learner, enrolls the
  same name again on the same disk and sees the members list it again with nobody
  touching the marker (about 12 s). `daemon_ledger_ownership_test` covers
  `main.ownership_of`.


## Returning from web history

`daemon/ui_socket.observer_path` admits a read-only click at
`web_view/component.latest_path`, beside the existing older-history control.
The handler retires the page's frozen ancestry endpoint and restores the
latest authoritative capture; it sends no session mutation. Admission remains
exact in path and event name, covered by `client/ui_socket_test`. The page's
actual handler table is pinned on both roles by web_view's `page_events_test`.
Protocol-change/051's history browsing addendum records this event admission.
