pins: a read/write noticeboard, the matrix’s dual

A pin is state a kit publishes to a keyed slot on a persistent register, the in-place dual of an event appended to scrollback. The transcript is an append-only log of things that happened, sealed once written; a pin is what is currently true — a task rollup, a build’s status, the file under edit — and state changes by being overwritten, not appended. exarch already redraws host-authored state in place (the agent matrix, the ctx% gauge, the phase bar — fixed marks, never streamed, never logged); a pin is the missing fourth cell, the same graphic grammar authored by the kit instead of the host. The register started write-only — a kit could publish state but never read it back — and is now read/write: pin-read and pin-list answer from the same mirror the nudge already kept, so the model (and a kit) can survey the board it writes to. The full reasoning for the wire and its taxonomy is surface-pins-state; the read side is register-is-read-write.

The vocabulary

A pin rides the same surface channel as everything else, as a disposition wrapper around an ordinary “ `card “ — the wrapper carries only placement, the card vocabulary and its decoder reused verbatim, and this wire is unchanged by the read side:

  • `pin [key, body] — write body to register slot key, overwriting in place on re-pin. The key is model-chosen and is the datum’s identity — the thing the host cannot guess. No key is reserved: the write side has no guard at all, and every slot in the register is model-authored.
  • `unpin [key] — drop the slot (a finished plan clears its gauge). An absent body is the same as `unpin.

The register once carried one host-owned exception, a services slot the host reconciled and ordinary surface writes could not touch (reject_protected_pin); that mechanism is deleted outright, on no technical rationale beyond the operator’s own — protected pins should not exist. Durable services lose the register as a legibility channel with it: what a service <desc> <thunk> birth still shows is the same worker-birth trail card an ordinary spawn gets (worker #id cmd durable, builtins) plus an aggregate running count on /resources — no per-service listing survives. See names-and-schedule-labels’s 2026-08-27 amendment.

On top of that wire sit four model-facing commands, one pin-* family: pin-set/pin-clear are ral wrappers over `pin/`unpin; pin-read/pin-list are the two Rust enquiries that make the register legible back (builtins). “Set” says a pin overwrites a slot, “clear” says it empties — not “add”/“remove”, which would say the register is a collection rather than one card per key.

So a kit holding evolving state pins one rollup and overwrites it, rather than marching tasks 0/3, tasks 1/3, … down the scrollback — the streaming the rail doctrine forbids. exarch/data/agent.ral (the tasks section) is the first client: transition reads the list back, computes the next one, and pins the gauge that fills in place — a per-task open → done move appends nothing to the transcript, and now appends nothing to a bound list either.

How it flows

One decoder, one new arm, one viewport field, and one enquiry class over the same mirror — no new concurrency invariant, because a pin is still emitted in-run through the live foreground sink, the exact place a card is already safe:

  • Decode. value_to_pin is tried first on the surface channel, ahead of the io and card arms (shell-eval); they cannot collide — io is a Map, the rest distinct Variant labels. It resolves to Surface::Pin { key, card } or Surface::Unpin { key }; the applier records a forensic breadcrumb and publishes the corresponding live Transient.
  • Register. The slots live on the `Viewport` as an ordered key → Card map; a pin is set_pin (overwrite or insert, first-seen order), an unpin is drop_pin — the in-place analogue of push_card, touching neither the flatten nor the log. reset clears it on /clear, so pins are generation-bounded exactly as scrollback is.
  • Mirror. The session keeps its own copy alongside the viewport’s, per agent: a key → PinDigest map (PinDigests, shell-eval) holding the full decoded Card, written on every accepted pin/unpin. It was born to let the nudge name what is pinned without parsing rendered text; the read side reuses the same store rather than adding a second one — pin-read and pin-list are enquiries answered straight from it.
  • Render. The register is a reserved right-hand column for the focused session — a flat strip glued to the right edge, never a floating overlay that would occlude the yank-able scrollback. It claims only dead margin past the READ_W reading cap, so the transcript never narrows; below a width threshold it collapses to a one-row pin band beside the matrix. The frame reads symmetrically: the rail owns the left edge (what happened), the register the right (what is).
  • Headless. There is no drawn register to overwrite, so a pin renders nothing. The record log retains Pin/Unpin as forensic breadcrumbs (one-seam-one-log) that no fold draws — the live register follows the shell boundary and is not restored on resume. Pinned state is ambient, like the matrix. pin-read/pin-list still answer headless, since they read the mirror, not the drawn column.

Reading the register back

pin-read <key> answers the card stored under key, canonically re-encoded, or () on a miss; pin-list answers the occupied keys. The encoder (encode_card, cards) is value_to_card’s inverse on the decoder’s image, so what comes back is never the authored bytes — a bare-string span or a bare mark, sugar the decoder accepts, comes back tagged, and an unknown mark comes back as the plain text it already degraded to. Canonical, not verbatim, closes the one hole a write-only register never had to worry about: an authored value could otherwise carry state the decoder discards, and the register would hold a truth the rail does not show. Reading the canonical card makes storage and display one thing by construction — the WYSIWYG invariant: only what the card renders can be read back, and two states that render identically are the same state to pin-read.

Read-after-write within one run is sound for free: under the identity binding IdentityDesk::enquire drains queued surface frames before answering a request (core/src/protocol.rs), and under the wire frame order does the same, so a pin written earlier in the same script is already in the mirror when the read is answered. Both enquiries are per-agent, same as the mirror they read — a sub-agent’s register is its own, and pin-read never crosses that line; a foreign-key read is deliberately out of scope (see the ADR’s open questions).

The model is the register’s default mutator

Because the register now reads as easily as it writes, the model can survey the board (pin-list), read a slot (pin-read), judge it, and revise it (pin-set) with no kit in the loop — a stray fact, a warning, a note is pinnable with zero apparatus, the same way any card is surfaceable with zero apparatus. A kit that owns a key instead treats its card as a serialization: read it back, destructure against its own schema, mutate, pin-set again. The tasks kit is the worked example (register-is-read-write, §4): its mutators take no list argument and thread none — add-task, transition, tag-task each read pin-read "tasks", decode it against the kit’s own row shape, and write the new rollup back through one sync-tasks write point, which clears the slot once no task remains open. A card under "tasks" the model wrote directly, in a shape the kit’s decoder does not recognize, fails the next kit call with a didactic fail naming the expected shape — the price of one keyspace shared between a schemaless mutator and a schema’d one.

The model watches its own pins

Because pinned state is something the user is watching on the rail, the nudge facility tells the model when it changes. The agent keeps the same small key → one-line summary mirror pin-read answers from, while the session is otherwise pin-blind and the events go straight to the frontend. There is one pinned-state nudge, uniform for every pin kind (a task, a goal, any other pinned state alike) and every agent role (the interactive trunk and a returning sub-agent alike): edge-triggered and budget-free, it fires once, on the first quiet completion after the register’s one-line digest differs from the one last told, and stays silent on every quiet completion that repeats an already-told digest. An emptied register is itself silent — unpinning only re-arms the edge, so a later re-pin fires again even at a digest told long before, and there is no fallback reminder that nudges an empty register toward set-goal/add-task: that advertisement is what let a completion, its own reminder, and the next completion cycle forever, and it is gone with the livelock it caused. The exception is actionability: while the agent has live descendants, the pin reminder waits for their results, because the agent has already delegated the next move. Restlessness in a returning agent is not this nudge’s doing — that is its separate, budgeted obligation to call reply, re-issued while unmet. The pin reminder itself speaks once per change and then falls silent, so a sub-agent that finishes without replying right as its pinned state changes is told of both in the same completion, but only the reply obligation is told again next time. This is the discipline pinning earns: a kit that publishes state to a slot the user watches is told once when that state moves, whether the agent holding it is the trunk or a sub-agent.

Why this shape

  • It is the next honest cut on the road already taken. documents → (operation vs appearance) → (render vs control) → (event vs state) → (write-only vs read/write): each step refined the surface taxonomy by one distinction, and this is the first that looks inside the state cell rather than beside it.
  • It is the dual of a thing already on screen. The register adds no new visual vocabulary — it lets a kit author what the host authors in the matrix (transcript-as-graphic), and now read it back the same way the host’s own gauges are always legible.
  • Position carries the distinction. Rendering state in a reserved column makes the plane’s horizontal axis — Bertin’s strongest variable — the event/state partition, legible in where the mark sits rather than implicit in which list it lands on.
  • It makes the doctrine expressible, and now enforceable. “Encode the changing datum as a fixed-position magnitude, never streamed” was enforceable only on host marks; a write-only pin let a kit obey it in appearance while a bound list drifted out of step behind the scenes. Canonical read-back removes the second copy: the card is the datum, so there is nothing left to drift.

See also

surface-pins-state (the original decision — wire format and taxonomy, write-only as first drafted), register-is-read-write (the read side this page now describes: pin-read, pin-list, the encoder, the canonical-form rule, and the tasks/goal kits as preludes over the family), surface-carries-documents (the `card body a pin reuses verbatim), transcript-as-graphic (the matrix this is the model-authored dual of, and the encode-don’t-stream doctrine), cards (the render document the body decodes through, and the encoder that inverts it), builtins (the pin-set/pin-clear/pin-read/pin-list family and the tasks kit built over it), frontend (the viewport register and the draw layout), shell-eval (the host sink, the pin-first decode, and the mirror pin-read answers from), agent (the nudge that reminds the model of its pins, from the same mirror pin-read reads), names-and-schedule-labels (the commitment keyspace retired, and its 2026-08-27 amendment records the later removal of services, the one protected slot that survived it), and exarch/data/agent.ral (the tasks section — the first client, now a pure prelude over the family).