import { EventEmitter } from "node:events"; import type { CotalEndpoint, EndpointRef, MembershipSnapshot, Presence, PresenceStatus } from "@cotal-ai/core"; export type FeedDelivery = "multicast" | "unicast" | "anycast"; /** One feed row. A same-sender/same-text unicast burst coalesces into a single entry with a * deterministic id (the first message's) and ts (the earliest), so a history prefill and the * live tap dedupe-by-id and order correctly. `text` is plain — the surface colors it. */ export interface FeedEntry { id: string; ts: number; from: EndpointRef; delivery: FeedDelivery; channel?: string; toService?: string; toIds?: string[]; toNames?: string[]; count?: number; /** This row arrived on an agent's structured-event channel rather than a chat channel. * * DERIVED ONCE, HERE, because the header's rule is that a surface lays this model out and never * re-derives it. Every surface that wanted to tell event traffic from chat would otherwise * re-spell the convention locally, and three copies of a classifier is three chances to disagree * about one channel. Absent on unicast and anycast rows, which carry no channel at all. */ events?: boolean; text: string; } export interface StatusCounts { working: number; waiting: number; idle: number; offline: number; } /** One message inside a DM thread (un-coalesced). */ export interface DmMessage { ts: number; from: string; to: string; text: string; } /** A conversation between a peer and one counterparty. */ export interface DmThread { with: string; role?: string; status: PresenceStatus; lastTs: number; messages: DmMessage[]; } /** A peer's whole DM footprint — every counterparty it has talked to. */ export interface DmPeer { name: string; role?: string; status: PresenceStatus; lastTs: number; conversations: DmThread[]; } /** Derived operator signals — golden-signal counts, who needs attention, the DM roll-up. */ export interface MeshSignals { counts: StatusCounts; /** Agents blocked / needing input, name-ordered. NOT age-ordered: presence carries no * status-transition time, so how long a peer has been blocked is not knowable from the wire. */ waiting: Presence[]; /** Oldest heartbeat among LIVE agents — a liveness signal ("is anyone going quiet?"), NOT a * blocked-duration. {@link Presence.ts} is republished every heartbeat, so it can never say how * long an agent has been waiting. Undefined when no agent is live. */ stalestLiveTs?: number; dms: DmPeer[]; } /** The broker-authoritative membership feed as THIS viewer last saw it. Two facts, kept apart so a * failed read can never be mistaken for an empty feed: `snapshot` is the last successful read; * `unreadable` is the reason the most recent read or watch attempt failed (a fact about the * viewer, cleared by the next successful read). A feed that does not exist (no delivery daemon: * the bucket is absent, or it was never written) is a snapshot with no `asOf` and no members, which * renders as traffic-only: a fact about the mesh, and a different one. */ export interface MembershipView { snapshot?: MembershipSnapshot; unreadable?: string; } export interface MeshSnapshot { agents: Presence[]; endpoints: Presence[]; /** `messages`: the broker's retained count (capped per sender, so it can stop climbing under * live traffic). `arrivals`: messages this viewer saw ARRIVE on the live tap since it started, * monotonic, dropped with the channel: the honest base for an unread badge. */ channels: { channel: string; messages: number; arrivals: number; }[]; feed: FeedEntry[]; /** Broker-authoritative channel membership (the delivery daemon's CONNZ view plus the durable * registry), as last read; the topology lens overlays it to show silent subscribers. */ membership: MembershipView; /** `msgsPerSec`: the rolling 1 s rate. `activity`: a fixed 15-bucket message-volume series over * the last 60 s (one ~4 s bucket each), oldest→newest, raw counts: the status bar's sparkline. */ rates: { msgsPerSec: number; activity: number[]; }; status: { connected: boolean; space: string; dmVisible: boolean; error?: string; warning?: string; }; signals: MeshSignals; nameOf: (id: string) => string; } export interface MeshViewOptions { /** Feed cap, in entries. Default 300. */ window?: number; /** The read-only tap subject. `chatWildcard(space)` narrows to multicast (auth — DMs/anycast * stay confidential); `spaceWildcard(space)` or undefined taps the whole space (god-view). */ tapSubject?: string; } /** * Events: * - `"entry"` `(e: FeedEntry)` — one live feed row, as it lands (for the stream renderer). * - `"presence"` `(ev: PresenceEvent)` — forwarded presence change (join/update/offline). * - `"warning"` `(e: Error)` — a recoverable endpoint notice for the stream renderer. * - `"change"` `(s: MeshSnapshot)` — a batched snapshot every ~75ms (for dashboards). */ export declare class MeshView extends EventEmitter { private readonly ep; private roster; private byId; private channelCounts; private arrivals; private feed; private seen; private pending; private dmLog; private recentTs; private msgsPerSec; private bucketCounts; private bucketEpoch; private connected; private error?; private warning?; private dirty; private membership; private membershipWatch?; private membershipTimer?; private readonly window; private readonly tapSubject?; private readonly chatOnly; private timers; private readonly onRoster; private readonly onPresence; private readonly onError; /** A rebuilt connection clears the fault that dropped the old one, so the view reports itself * connected again instead of keeping that error for good. */ private readonly onConnection; private readonly onWarning; constructor(ep: CotalEndpoint, opts?: MeshViewOptions); start(): Promise; stop(): Promise; /** Read the broker-authoritative membership feed and follow it. A membership fault is never * a connection fault: it lands on `membership.unreadable` (with its reason), not on * `status.error`, because the mesh is fine and only this viewer's read of one feed is not. */ private startMembership; private scheduleMembership; /** One read of the feed. Three outcomes, kept distinct: a snapshot (readable, whatever it holds), * an absent feed (readable in the sense that the mesh has no daemon writing one: an empty * snapshot, traffic-only), and a failed read (`unreadable`, reason kept, last snapshot kept so * the reason is shown against what was last known rather than against nothing). */ private loadMembership; snapshot(): MeshSnapshot; nameOf: (id: string) => string; private setRoster; /** Live read-only tap. Drops control/presence/trace frames (deliveryOf → null). A payload * from.name is sender-chosen display text, not identity (#1399): the roster is the only writer * of id→name, so a spoofed name can never re-key a thread or a label. */ private ingest; /** Group a same-sender/same-text unicast burst into one feed entry over BURST_MS, and retain * each raw message for the DM roll-up. */ private coalesce; private flushBurst; /** Append to the feed, dedupe by id, keep it windowed, and notify stream consumers. */ private push; private trim; private recordDm; /** One-shot backlog: prefill each channel's history (multicast), plus the DM backlog when DMs * are visible (god-view — `dmHistory` needs an admin cred), deduped by id, oldest-first. */ private prefill; /** Best-effort DM backlog for the roll-up — only meaningful for a god-view cred; a non-admin * observer's `dmHistory` throws (ACL), which just leaves the DM lens live-only. A history * row's from.name is payload text, never an id→name source (#1399). */ private prefillDms; private refreshChannels; private signals; /** Group raw DMs into per-peer rows; each peer lists its counterparties (conversations). * Only pairs that actually talked — never the n² cross-product. Conversations key on the two * AUTHENTICATED ids; display names, roles and statuses resolve through the roster by id, so a * payload `from.name` cannot move a line into another peer's thread (#1399). */ private rollupDms; /** Advance the activity ring to `now`: shift past buckets out the left, fill the gap with zeros, * so idle time decays the series. The newest (last) bucket is always the current ~4s window. */ private roll; /** The single batch point: recompute the rolling rate, then emit one snapshot if dirty. */ private flush; } //# sourceMappingURL=mesh-view.d.ts.map