Skip to content

MeshView

Reference: TypeScript observer surfaces (MeshView) · For: integrators building a watch surface · Wire contract: SPEC

MeshView is the shared model behind every surface that lets a human watch a live mesh: the terminal console, the plain stream, and the web dashboard. It defines what those surfaces show and keeps them from drifting apart.

Reference-implementation boundary. MeshView is an observer API. The wire remains the source of truth; every field below is a rendering derived from it. A different client is free to derive its own model or none at all; nothing here is normative. What is normative (subjects, delivery modes, presence) lives in the SPEC.

Every surface is built on one read-only observer: a CotalEndpoint started with consume: false, registerPresence: false, watchPresence: true, invisible to peers, binding no durables, reading the space through the live tap plus history and presence-watch. No surface opens its own NATS connection, and none re-implements the wire semantics. On an open mesh the console adds a second, presence-only endpoint under the observer’s card the moment the operator sends, so agents can reply (see watch a mesh); a pure-watch session stays the invisible observer.

One class (implementations/cli/src/view/mesh-view.ts) consumes that observer and emits a normalized, render-agnostic model: no ANSI, no React, no HTML, no colour, pure data. It owns the endpoint lifecycle (start → tap → stop) and batches every source (roster events, the tap, burst flushes, channel polls, the rate/age heartbeat) into one snapshot per ~75 ms tick.

new MeshView(ep, { window?, tapSubject? })
.on("entry", (e: FeedEntry) => …) // one classified+coalesced row, as it lands (stream)
.on("presence", (ev) => …) // a forwarded presence change (join / update / offline)
.on("change", (s: MeshSnapshot) => …) // a batched snapshot (~75 ms) for dashboards
await view.start();
view.snapshot(); // pull the current model on demand
await view.stop();

window caps the feed (default 300 entries). tapSubject chooses visibility: chatWildcard(space) narrows the tap to multicast (auth: DMs and anycast stay confidential); spaceWildcard(space) or omitting it taps the whole space (the god-view).

interface FeedEntry { // one feed row
id: string;
ts: number;
from: EndpointRef;
delivery: "multicast" | "unicast" | "anycast";
channel?: string; // multicast target
toService?: string; // anycast target
toIds?: string[]; // unicast: authoritative target endpoint ids
toNames?: string[]; // unicast: targets resolved off the roster
count?: number; // unicast: burst multiplicity for a coalesced entry
text: string; // parts joined, plain; the surface colours it
}
interface MeshSnapshot {
agents: Presence[]; // card.kind === "agent", status-sorted (working→waiting→idle→offline) then by name
endpoints: Presence[]; // everything else
channels: { channel: string; messages: number; arrivals: number }[]; // retained count; live arrivals seen by this viewer
feed: FeedEntry[]; // classified + coalesced + windowed
membership: { snapshot?: MembershipSnapshot; unreadable?: string }; // the feed as last read (below)
rates: { msgsPerSec: number; activity: number[] }; // rolling 1 s rate; 15 x 4 s volume buckets (60 s)
status: { connected: boolean; space: string; dmVisible: boolean; error?: string };
signals: MeshSignals; // derived operator signals (below)
nameOf: (id: string) => string; // unicast target id → display name
}

What the model does:

  • Classification. deliveryOf(subject) returns chat / unicast / anycast (chat renders as multicast); control, presence, and trace frames return null and drop out of the feed.
  • Coalescing. A same-sender/same-text unicast burst within 400 ms collapses to one entry, with a deterministic id (the first message’s), ts (the earliest), count (the multiplicity), and parallel toIds/toNames arrays so renderers keep identity separate from labels.
  • Roster. A status-sorted snapshot plus an id→name map; agents split from other endpoints.
  • Topology identity. Agent nodes use endpoint ids as keys and names only as labels, so two principals with the same display name keep separate traffic and membership links.
  • History prefill. A one-shot per-channel backlog (multicast; plus DM backlog when DMs are visible), deduped against the live tap by id.
  • Windowing. The feed is capped (~300 entries) with a rolling msgs/s rate.
  • Activity series. rates.activity is a fixed 15-bucket, 60-second message-volume series (one 4-second bucket each, oldest first, raw counts) that decays on the tick even when nothing arrives. The console draws it as a sparkline in the status bar next to msgs/s.
interface MeshSignals {
counts: { working: number; waiting: number; idle: number; offline: number }; // golden-signal tiles
waiting: Presence[]; // agents blocked / needing input, name-ordered
stalestLiveTs?: number; // oldest heartbeat among live agents (liveness, not blocked-duration)
dms: DmPeer[]; // per-peer DM roll-up (only populated when DMs are visible)
}

How waiting is ordered. Presence.ts is the last heartbeat, republished on every beat (2 s by default). Presence.statusSince is when the agent entered its current status and activity, so an edit to the activity of a waiting agent moves it too. It dates the report, not the block, so “how long has this agent been blocked” is not knowable from presence, and no surface may claim it. waiting is therefore name-ordered, and the fifth golden-signal tile reports stalestLiveTs: the oldest heartbeat among live agents, which answers “is a peer going quiet?” and self-clears when that peer drops to offline. Offline agents are excluded: their heartbeat age only grows, so including them would pin the tile to an ever-increasing number that can never be acted on.

dms groups unicast traffic into per-peer conversations (DmPeer → DmThread → DmMessage), only the pairs that actually talked, never the n² cross-product. It is populated only when DMs are visible (god-view / open mode); a chat-only observer leaves it empty.

membership carries the feed as this viewer last saw it, as two facts kept apart: snapshot, the last successful read, and unreadable, the reason the most recent read or watch attempt failed. A watch that closes after it was already armed reports through this same unreadable fact, not through a connection fault, so the next successful read clears it the same way a failed read would. The topology lens turns them into one of four pill states, checked in this order:

Pill Means
unreadable (reason shown) the read or the watch itself failed: a fact about this viewer, never shown as an empty mesh; the overlay is withheld
traffic-only no daemon writes a feed here (the bucket is absent, or was never written): the lens is traffic-derived, an honest mesh fact
live a snapshot whose heartbeat (asOf) is younger than 45 seconds
stale a snapshot older than that, or one with no heartbeat at all

A membership fault never lands on status.error: the mesh is fine, and only this viewer’s read of one feed is not.

Feature Model field console (Ink) stream web
roster (status, activity, age) agents / endpoints ✓ panel ✓ presence lines ✓ sidebar
all-activity feed feed ✓ feed panel ✓ log ✓ Monitor view
channels plus counts, unread badges channels (+ client state) ✓ tabs (1–9, +N) ✓ sidebar + Channel view
golden-signal counts signals.counts ✓ tiles strip ✓ tiles
needs-you / blocked signals.waiting ✓ rail (n) ✓ NEEDS-YOU rail
direct-message lens signals.dms ✓ lens (d) ✓ DM view
topology (membership plus who-talks-to-whom) feed + agents + membership ✓ lens (t, 3 variants) ✓ graph
message / agent detail (incl. harness, model, skills) feed / agents ✓ select → detail ✓ row / thread
search / filter client ✓ / (grep) ✓ mode chips
msgs/s, activity sparkline, connected, dmVisible rates / status ✓ status bar ✓ conn pill
attention mode (dnd / focus) agents[].attention ✓ roster + detail + graph
per-channel attention (quiet / muted) agents[].channelModes ✓ agent detail
harness, model, variant agents[].card.meta ✓ roster tag + detail ✓ badges + graph
host (which machine it runs on) agents[].card.meta.host ✓ agent detail
channel policy (replay, delivery class) /api/channels (web) ✓ sidebar + header chips

Each interactive surface renders the fields its column above marks; rates.activity is the console’s alone. The console adds the signals as an always-on tiles strip, a NEEDS-YOU rail (n), and a DM lens (d); the topology lens (t) collapses the feed plus roster into a who-talks-to-whom graph client-side and renders it three switchable ways (v / 1–3): swimlane sequence, adjacency heat matrix, and a ring node-link map. It also overlays the broker-authoritative membership feed (readMembership / watchMembership, the same source as the web graph): silent subscribers appear as nodes, subscriptions as resting spokes (live solid and faint, durable-offline dashed), and wide readers (> / *) carry a ≫ badge. The map draws the full skeleton, the matrix a light ∘ / ◌ marker, the sequence stays a traffic timeline. Channel tabs carry a per-channel unread badge (+N, viewer-local: messages since that channel was last viewed, the same kind of state as the web sidebar’s pill; a deleted channel’s tab leaves the strip on the next channel poll, with no badge on the way out), the roster tags each agent with its harness when known (cc for Claude Code, oc for OpenCode, and so on, from the card’s meta.connector or, for a managed seat whose card carries none, the manager’s launch record), and the agent detail adds runs, model, and skills from the same sources. The stream is line-oriented, so the signals stay out of it.

The web’s ?demo scene also mocks features that no protocol message backs yet. They render only as the static design reference, never from live data, and are deliberately not implemented on the live surfaces, design intent until the wire grows to support them:

Flourish What it would need
intent badges (“about to act”) a new intent message kind / field on the wire
approval requests (approve / deny) a request message kind plus a response path (interactive)
task-failed alerts a failure signal: a manager lifecycle event or a presence status
unclaimed-anycast / status roll-up mostly derivable from existing traffic; a MeshView signal
  • Derive once, render many. Classification, coalescing, sorting, id→name, rate, windowing, and the operator signals all live in MeshView. A surface only lays out the model; it never re-derives it. New surfaces are thin clients.
  • Presentation stays per-surface. Colour palette, layout, CSS, keybindings, and input handling belong to each renderer, not the model.
  • No fallbacks. If the observer cannot do what a surface needs, throw; do not silently degrade.
  • Status is shape and colour. ● working · ◐ waiting · ○ idle · ⨯/⊘ offline, never colour alone (accessibility).
  • Never render what the wire cannot say. A surface shows a value only if the protocol actually carries it. Where it does not, say so plainly. An agent whose harness never reported a model reads “not reported”, never a guessed default; a heartbeat age is labelled as a heartbeat age, never as a blocked-duration. A confident wrong number costs more trust than an honest gap.
  • open attention is silent. attention: "open" and an absent attention mean the same thing (receives everything), so neither renders a badge. Only dnd and focus surface. A marker on every peer is noise, and the point of the signal is that it stands out.

For the operator-facing walkthrough of these surfaces, see Watch a mesh.