/** * tmux's three id kinds, kept apart by the type system. * * tmux prints a sigil on every id -- `session=$25 window=@75 pane=%89` -- but * they are all strings, so murmur could and did pass one where another was * meant. Twice, in shipped code: a sweep keyed on window liveness deleted ten * live agents, and a window cached at extension startup badged the window a * moved pane had left. * * An agent is addressed by its tmux server and PANE, which survives `move-pane`, * `break-pane` and a window closed and reopened. A session and a window are only * where that pane currently lives and may differ between two reports from one * agent. Hence the rule the brands enforce: only a pane may decide whether an * agent exists. * * Compile-time fiction -- at runtime these are the strings tmux printed, which * keeps the snapshot document and every stored row byte-identical. */ declare const brand: unique symbol; /** A tmux session id, `$N`. Mutable location. */ type SessionId = string & { readonly [brand]: "session"; }; /** A tmux window id, `@N`. Mutable location -- never an agent's identity. */ type WindowId = string & { readonly [brand]: "window"; }; /** A tmux pane id, `%N`. Unique within one tmux server. */ type PaneId = string & { readonly [brand]: "pane"; }; declare function asSessionId(raw: string): SessionId; declare function asWindowId(raw: string): WindowId; declare function asPaneId(raw: string): PaneId; /** * The three independent facts, as types. * * `activity` is what the pane's own process says it is doing. `attention` is * whether a human is wanted. `freshness` (src/view.ts) is how recently we * reached the node that reported. They are three independent fields, never one * enum, and absence carries meaning: no attention row means "nothing to see", * no agent row means "no agent here". */ type Activity = "running" | "stopped"; type AttentionKind = "done" | "blocked" | "crashed"; /** * Who is waiting on this agent -- a human, or a supervisor that consumes the * result. Not "which harness"; that is `cli`. */ type Driver = "human" | "orchestrated"; declare const DEFAULT_DRIVER: Driver; type TmuxServer = { kind: "default"; } | { kind: "label"; value: string; } | { kind: "path"; value: string; }; /** * Where a pane currently lives. Location, never identity. * * `pane` is the address and is stable for the life of the pane; `session` and * `window` are only where that pane currently is, and both change under * move-pane and break-pane. Only a pane may decide whether an agent exists, * which is what the brands in ./ids.js enforce. */ type PaneIdentity = { server: TmuxServer; pane: PaneId; }; type Location = PaneIdentity & { session: SessionId; window: WindowId; pane: PaneId; session_name: string | null; window_name: string | null; }; /** * pi's thinking levels, verbatim. * * A closed set, so `effort` can be CHECK-constrained in the schema and * `member()`-validated on the wire, the same way `activity` and `driver` are. * A display variant like "Medium" is a broken peer rather than a value to * coerce. * * A VALUE with the type derived from it, not a hand-written union, because four * places enforce this vocabulary: this type, the wire validator, the producer's * filter, and the SQLite CHECK. Written out four times it drifts, and the three * failures look nothing alike -- the producer silently drops a valid report, the * validator rejects a good peer snapshot, the store rejects a local write. This * repo has already paid for a duplicated protocol rule once: `SNAPSHOT_VERSION` * was four literals, and the copy in `peer.ts` made `peer list` call every * correctly upgraded peer incompatible. * * The schema is the one unavoidable second spelling, since SQL is a string and * cannot import. `test/architecture.test.ts` fails if a THIRD appears. */ declare const EFFORTS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"]; type Effort = (typeof EFFORTS)[number]; /** * Tokens and money for one agent's session, as the provider reported them. * * A SUB-OBJECT rather than twelve more columns, and nullable as a unit. Three * reasons, in order of how much they cost when ignored: * * 1. It arrives as a bundle. pi hands over one `Usage` object per turn, so * every field in here shares one clock and one provenance -- and a partial * write mixing this turn's cost with last turn's tokens would be a number * nobody could interpret. * 2. Absence is a real state. An agent that has not completed a turn has no * usage at all, which is different from having zero tokens, and flat columns * would have to spell that with twelve nulls. * 3. Nothing renders it yet. These are for a display concern that does not * exist: the card draws `model`, `effort` and `context_pct` only. Carrying * the numbers now means the next display option costs no wire change, which * is the whole argument for collecting them early -- and the wire is not * compatible across versions, so adding a field later costs a coordinated * upgrade of every node in the fleet. * * `cache_write_1h` and `reasoning` are optional WITHIN the object because only * some providers report them -- Anthropic splits cache retention, and a * reasoning breakdown exists only where the model exposes one. Absent means the * provider said nothing, which is not the same as zero. */ type AgentUsage = { input: number; output: number; cache_read: number; cache_write: number; total_tokens: number; /** Subset of `cache_write` with 1h retention. Anthropic only. */ cache_write_1h: number | null; /** Thinking tokens, already counted in `output`. Provider-dependent. */ reasoning: number | null; cost_input: number; cost_output: number; cost_cache_read: number; cost_cache_write: number; cost_total: number; }; /** * What the agent is running with, as the agent reports it. * * A SIBLING of `AgentMeta`, not more keys on it, and the split is the point. * `AgentMeta` is what an owner asserts once when it claims a pane -- name, * session, workstream, role, cli, driver -- and `claimAgent` is its only * writer. Nothing in it changes for the life of the process. * * These fields change repeatedly, from four report sites -- a model change, an * effort change, a completed turn, and a resumed session -- and `claimAgent` * never sees any of them. Folding them in would mean a "metadata" type half of * whose fields are live state, and the next reader could not tell which half * they were holding. * * All nullable: a bare shell, codex, or a notify-only harness reports none of * them, and an agent that cannot report one must not be forced to invent it. */ type AgentRuntime = { /** Model id with the provider prefix stripped, e.g. `claude-opus-5`. */ model: string | null; effort: Effort | null; /** * Percent of the context window in use, 0..100. * * Null is a normal state, not merely an absent one: pi reports a null percent * immediately after a compaction, before the next response. */ context_pct: number | null; /** Absolute context figures, beside the percentage they were derived from. */ context_tokens: number | null; context_window: number | null; /** The provider id, kept apart from `model` so neither has to be parsed out. */ provider: string | null; /** * The effort the provider actually applied, when it says so. * * Distinct from `effort`, which is what was REQUESTED: a model can clamp a * requested level, and the two disagreeing is a fact worth being able to see * rather than a contradiction to resolve. Free text, because this is the * provider's own vocabulary and not pi's closed set. */ provider_effort: string | null; /** Tokens and money, or null when no turn has completed. See `AgentUsage`. */ usage: AgentUsage | null; }; /** Owner-reported metadata about the agent in a pane. */ type AgentMeta = { agent_name: string | null; pi_session: string | null; workstream: string | null; role: string | null; cli: string; driver: Driver; }; type PeerRecord = { name: string; target: string; /** Opaque command template for interactive access; `{attach}` or `{pane}` is substituted. */ jump_command: string; host_id: string | null; display_name: string | null; /** The whole validated document, or null when we have never parsed one. */ snapshot: Snapshot | null; /** The PEER's clock: when that node built the document. */ snapshot_at: number | null; /** OUR clock: when we last reached it. Freshness is computed from this. */ fetched_at: number | null; last_attempt_at: number | null; last_error: string | null; murmur_version: string | null; /** The peer's `murmur_snapshot` value, i.e. the document version it speaks. */ snapshot_version: number | null; }; /** * The snapshot document version this node speaks. * * Declared beside the type it describes, and the ONE place the number lives. * It was previously a literal in `Snapshot`, in `buildLocalSnapshot`, in * `parseSnapshot`'s check and again in `peer.ts` -- four copies of one fact, * which drifted the moment the version changed: `peer list` went on reporting * that every peer speaking the new version was incompatible, because its copy * still said 1. */ declare const SNAPSHOT_VERSION = 3; /** * One node's whole current state. Complete, never a delta: a peer that returns * one has said everything it knows, so absence from it is absence. */ type Snapshot = { murmur_snapshot: typeof SNAPSHOT_VERSION; host_id: string; display_name: string; murmur_version: string; generated_at: number; panes: SnapshotPane[]; }; type LocalPane = SnapshotPane; type SnapshotPane = { server: TmuxServer; pane: PaneId; session: SessionId; window: WindowId; session_name: string | null; window_name: string | null; /** Null for an attention-only pane: valid, listable, jumpable. */ agent: SnapshotAgent | null; attention: SnapshotAttention[]; }; type SnapshotAgent = AgentMeta & AgentRuntime & { agent_id: string; activity: Activity; claimed_at: number; updated_at: number; }; type SnapshotAttention = { kind: AttentionKind; message: string; source: string; requested_at: number; }; /** * Whether a pid is still running. A parameter everywhere it is consulted, so a * test needs no process table. */ type LiveCheck = (pid: number) => boolean; type AgentClaim = { location: Location; owner_pid: number; meta: AgentMeta; now?: number; isAlive?: LiveCheck; }; type ClaimResult = { outcome: "claimed"; agent_id: string; } | { outcome: "retained"; agent_id: string; } | { outcome: "replaced"; agent_id: string; previous_agent_id: string; } | { outcome: "refused"; held_by_pid: number; }; type ActivityUpdate = { agent_id: string; owner_pid: number; activity: Activity; location: Location; now?: number; }; /** * A runtime report from an agent about itself. * * `Partial`, because the fields arrive from three different pi events -- a model * change, an effort change, a completed turn -- and a call that had to pass all * of them would force the producer to invent the ones it did not just learn. * Re-asserting a stale model on a context update is the bug this shape prevents. * * Only the keys PRESENT are written, so an explicit `null` (pi reports one for * the context percent right after a compaction) is distinct from omission. * * Keyed on `agent_id` AND `owner_pid`, the same gate `ActivityUpdate` uses: a * pi nested in an agent's pane inherits $TMUX_PANE and must not be able to * report as the agent that owns it. */ type RuntimeUpdate = Partial & { agent_id: string; owner_pid: number; now?: number; }; type AgentRelease = { agent_id: string; owner_pid: number; location: PaneIdentity; }; /** * The kinds an EXTERNAL writer may request. * * `crashed` is deliberately absent. It is reconciliation's word: it means "the * owning process died without saying so", which only the node that can probe * that pid may conclude. A caller asserting it would be manufacturing a fact it * cannot observe, and the rule was previously only a convention -- nothing * stopped `requestAttention({ kind: "crashed" })` from an extension, a notify * hook, or a future surface, and a sweep confirmed such a row lands in * `localPanes` indistinguishable from a real crash. * * The narrower type is the enforcement. Reconciliation writes its own row * through the same statement without going through this shape. */ type RequestableKind = Exclude; /** * Everything an attention writer may say. There is no agent_id, no owner_pid, * no activity and no owner metadata field, and adding one is a contract change. */ type AttentionRequest = { kind: RequestableKind; location: Location; message: string; source: string; now?: number; }; /** * The only local facts reconciliation is allowed to consult. * * `panes` is null when tmux could not answer, which is not evidence of death. * `isAlive` and `now` are parameters so a test needs no process table and no * clock control. */ type LocalWorld = { server: TmuxServer; panes: Set | null; isAlive?: LiveCheck; now?: number; }; type ReconcileSummary = { crashed: PaneId[]; removed: PaneId[]; attention_removed: PaneId[]; }; type PeerFetch = { ok: true; snapshot: Snapshot; at: number; } | { ok: false; error: string; at: number; }; type NodeIdentity = { host_id: string; display_name: string; }; /** * This node's identity, or null when it has none. * * A READ, and only a read: nothing mints here. Every command that needs a * host_id fails with "murmur is not initialised on this node; run: murmur init" * rather than bringing a node into existence as a side effect of a status-bar * tick. */ declare function loadIdentity(): NodeIdentity | null; /** Create this node's identity. Only `murmur init` calls it. */ declare function createIdentity(displayName?: string): NodeIdentity; /** * Rename an existing node, keeping its `host_id`. * * `murmur init --name` on an already-initialised node used to ignore the flag * silently, which is the one thing a rename must not do. */ declare function setDisplayName(displayName: string): NodeIdentity; /** * The store, and the only place in murmur that holds a database handle or * writes SQL. * * This interface is CLOSED: no `append`, no `ingest`, no log read, no partial-row * update, and no local read other than `localPanes`. Each of those shapes lets a * writer say something it has no standing to say, and each cost a shipped bug. * Attention methods take no agent identity, which is what makes "a notifier * cannot corrupt an agent row" structural. */ interface Store { claimAgent(claim: AgentClaim): ClaimResult; setActivity(update: ActivityUpdate): boolean; /** * Record what the agent is running with. Owner-gated, partial. * * Separate from `setActivity` because these are different claims by the same * owner: activity is what the process is doing, runtime is what it is doing it * with. One call that carried both would have to be given every field on every * event, and three of pi's four report sites know only one of them. */ setRuntime(update: RuntimeUpdate): boolean; releaseAgent(release: AgentRelease): boolean; requestAttention(request: AttentionRequest): void; /** * Record a crash for a pane, as reconciliation would. * * `crashed` is not in `AttentionRequest` because it is reconciliation's word: * it asserts an owning process died without saying so, which only the node * that probed that pid may conclude. This is the same statement reconciliation * uses, named so that a caller reaching for it has to mean it -- tests seeding * a crashed row are the honest use, and anything else in production would be * manufacturing a fact it cannot observe. */ recordCrash(location: Location, now?: number): void; acknowledgePane(location: PaneIdentity): number; /** The one local read. Joins agents and attention by server and pane. */ localPanes(): LocalPane[]; reconcileLocal(world: LocalWorld): ReconcileSummary; buildLocalSnapshot(identity: NodeIdentity, worlds: LocalWorld | readonly LocalWorld[]): Snapshot; peers(): PeerRecord[]; addPeer(name: string, target: string, jumpCommand?: string): void; setPeerJumpCommand(name: string, jumpCommand: string): boolean; removePeer(name: string): boolean; replacePeerSnapshot(name: string, fetch: PeerFetch): void; close(): void; } /** * Open the store. Takes no arguments and mints no identity. * * `openStore` deliberately does NOT read or create `identity.json`: identity is * created only by `murmur init`, so a read path — a status-bar tick, a focus * hook — cannot bring a node into existence as a side effect. */ declare function openStore(): Store; type Freshness = "fresh" | "stale"; /** * What a surface paints. Presentation only, derived from the three independent * facts and never stored. */ type RenderState = "crashed" | "blocked" | "done" | "running" | "idle"; /** * THE single ordering table: which state matters most, for sorting and for * choosing one word to show. * * `status.ts` and `pick.ts` import this rather than declaring their own copies, * so no two surfaces can sort one list differently. */ declare const RENDER_PRIORITY: readonly RenderState[]; /** * The attention kinds only a human can answer, and the second table both * surfaces must agree on. * * `blocked` means waiting for an answer an orchestrator cannot give -- mu places * work, it cannot choose between two approaches. `crashed` means the process * died. Everything else about an orchestrated agent is its supervisor's * business. * * `pick.ts` uses it for which crew rows are visible by default, `status.ts` for * which crew states reach the status bar. Two literals in two files answering * one question is how a row that needed a human became one a human could not * see. */ declare const NEEDS_HUMAN: readonly AttentionKind[]; /** * One attention request as a surface reads it: the kind, and WHEN it was asked. * * The timestamp is per kind rather than folded into the pane's `updated_at`, * because urgency is per request: a pane crashed an hour ago and blocked ten * seconds ago has two different ages, and the one that decides its position is * the one belonging to the kind it renders as. Collapsing them to a single * `max` -- which is what `updated_at` is -- let a fresh `done` mask a starving * `blocked` on the same pane. * * `message` is carried for the dash card one-liner. `source` stays in the * snapshot because no surface paints it. */ type PaneAttention = { kind: AttentionKind; requested_at: number; message: string; }; /** * One pane, as every surface reads it: address, the three independent facts, * owner metadata, and ages. * * Local and remote panes are the same type, built by the same mapping, because * `Store.localPanes()` and a peer's cached snapshot both return * `SnapshotPane[]`. One mapping means local and remote cannot drift apart. */ type PaneView = { host_id: string; /** The name the operator typed, or this node's display_name. */ host: string; local: boolean; server: TmuxServer; pane: PaneId; session: SessionId; window: WindowId; session_name: string | null; window_name: string | null; /** Null for an attention-only pane, which has no agent row. */ activity: Activity | null; attention: PaneAttention[]; freshness: Freshness; agent_id: string | null; agent_name: string | null; pi_session: string | null; workstream: string | null; role: string | null; cli: string | null; model: string | null; provider: string | null; effort: Effort | null; provider_effort: string | null; context_pct: number | null; context_tokens: number | null; context_window: number | null; usage: AgentUsage | null; driver: Driver; /** When the pane's own node last said something. Never `fetched_at`. */ updated_at: number | null; /** When that node generated its snapshot. Null for local. */ snapshot_at: number | null; /** When we last reached that node. Null for local. */ fetched_at: number | null; /** A local pane already attached to this remote worker, derived per read. */ attached_pane: PaneId | null; }; /** * How long a peer may go unfetched before its panes render stale. * * DEFINED here, because freshness is a view concept: the collector re-exports it * for callers already importing from there. The comment this replaces claimed * the opposite direction and sent a reader to collector.ts for a rationale that * was written nowhere. * * Sixty seconds is a ceiling on how wrong the HUD may be about a reachable host, * chosen against the collect cadence rather than derived: it must leave room for * a peer to miss two consecutive ambient attempts before it reads stale, so a * single slow poll does not make a healthy fleet flap. * * That is a real coupling and the compiler cannot see it: * `COLLECT_FLOOR_MS + COLLECT_JITTER_MS / 2` must stay strictly under this, or an * unlucky jitter draw pushes a reachable peer over the line on its own. * test/collector.test.ts asserts both halves of that inequality, so changing * either number fails a test rather than silently making the HUD flap. */ declare const STALENESS_MS = 60000; /** * A duration as the shortest thing worth reading: "5m", "2h", "3d". * * Under a minute is the empty string: an age that changes every second is noise * in a status column. This and `freshness` are the only two places a duration * becomes text or a verdict. */ declare function age(ms: number | null): string; /** * Freshness of a NODE, never of an agent. * * A peer we have never reached is stale rather than fresh: null means the first * collect has not succeeded yet, and an unreachable host you just added must not * render as up to date. */ declare function freshness(fetchedAt: number | null, now: number, thresholdMs?: number): Freshness; /** * One word for a pane. Attention wins over activity, because attention is a * request and activity is a description. * * A running agent with `blocked` attention is valid and expected, and surfaces * that can show both do -- this is only for the ones that must pick one. */ declare function renderState(view: { activity: Activity | null; attention: readonly { kind: AttentionKind; }[]; }): RenderState; /** * Every pane this node knows about: its own, plus one cached snapshot per peer. * * `identity` is non-null because every caller is a command that already requires * `murmur init`, so no pane can be misclassified as remote by an absent one. * * No liveness is probed here, for local or remote. A remote pane's `activity` is * whatever its own node last said; a stale node keeps its last-known fields * verbatim beside an explicit warning. */ declare function paneViews(store: Store, identity: NodeIdentity, now?: number): PaneView[]; /** What a reader knows about themselves. Everything optional; all of it a nudge. */ type SortContext = { now?: number; /** The pane the reader is sitting in, if they are in one. */ here?: string; }; /** * Attention-first ordering, then confidence, then urgency, then address. * * The state band is still lexicographic and still `RENDER_PRIORITY`: nothing * below may lift an `idle` row above a `blocked` one, because the band is the * one thing a reader is entitled to read off a position. Everything else * decides only who leads WITHIN a band. * * Two demotions are categorical rather than scored, because both are statements * about whether the row is worth acting on at all: * * - The pane you are SITTING IN goes last in its band. You do not need a * picker to reach the pane your cursor is already in, and it was reliably at * the top -- it is the pane that most recently said something. * - A row from a STALE host goes below every fresh row in its band. Its fields * are last-known and may be hours dead; a fresh row we can vouch for should * be reached first. Scoring this instead would have let an ever-growing age * on an unreachable host outrun every fact we can still verify. * * TOTAL on purpose, which is why the address comparisons remain. Ties are * ordinary -- two crashed panes reconciled in one transaction share a * `requested_at` exactly -- and `sort` is stable only with respect to the order * it was GIVEN, here whatever SQLite and the peer loop produced. An unbroken tie * makes the list depend on that: a status bar reshuffles between two identical * ticks, and a picker row moves under the keypress aimed at it. * * Presentation only. No caller may read meaning into a row's position -- pane * order in a snapshot carries none either, so a reader sorts for itself. */ declare function viewSort(views: PaneView[], context?: SortContext): PaneView[]; type LocalPaneProcess = { pane: PaneId; current_command: string; arguments: string; }; interface Mux { currentWindow(): Location | null; livePanes(server?: TmuxServer): Set | null; localPaneProcesses(): LocalPaneProcess[]; setWindowBadge(window: WindowId, state: RenderState | null, server?: TmuxServer): void; attach(pane: PaneId, server?: TmuxServer): boolean; windowForPane(pane: PaneId, server?: TmuxServer): WindowId | null; panesInWindow(window: WindowId, server?: TmuxServer): PaneId[]; capture(pane: PaneId, lines?: number, server?: TmuxServer): string | null; clientName(): string | null; currentTarget(): string | null; sessionNamed(name: string): boolean; newSession(name: string, command: string): boolean; setSessionOption(session: string, option: string, value: string): void; switchClient(client: string | null, session: string): boolean; markDashPane(pane: PaneId): void; unmarkDashPane(pane: PaneId): void; dashPane(): PaneId | null; clientIdentity(): string | null; jumpClientMarker(): string | null; armJumpMarkerCommand(): string; detachClient(client: string): boolean; } declare const tmux: Mux; declare function pidAlive(pid: number): boolean; /** * The most specific human-readable name a pane's agent has, never a tmux id. * * Four sources, most to least specific: mu's agent name, pi's session name, the * tmux window name, the tmux session name. All are recorded by the node owning * the pane, so a local and a remote row read the same -- a reader cannot resolve * a remote window id against its own tmux. * * The session name is shortened to its last segment, and that last rung is * reached far more often than it looks: `agent_name` is mu-only, `window_name` * is null whenever tmux is auto-renaming (its default), and `pi_session` is * null for the whole life of an unnamed session, because pi's auto-namer runs * at CLOSE. So the common row for a hand-started pi printed `hacking/murmur` -- * a path where a name belongs. * * The full name is not lost: the picker's stream column shows it, and shows it * BECAUSE of this shortening, since that column blanks when it would repeat the * name. Shortened, the row reads `murmur` + `hacking/murmur` -- same width, * strictly more information. * * Falls back to the window id only when a node recorded no names at all, which * means a non-tmux harness. */ declare function agentLabel(agent: PaneView): string; /** * Where the pane lives, for the second column. Names only -- the ids are what * jumps, not what a human reads. */ declare function agentLocation(agent: PaneView): string; declare function shellQuote(value: string): string; /** * The process calls jump makes outside tmux: the remote probe and the configured * interactive command. Injectable so the jump decision * table is testable without an ssh binary or a live peer -- without this seam, * replacing `jumpToAgent`'s body with `return { ok: true }` kept every jump * test green. */ type Runner = (file: string, args: string[], inherit?: boolean) => { status: number | null; stdout: string; failed: boolean; }; type JumpResult = { ok: true; } | { ok: false; reason: "no_peer" | "unreachable" | "no_tmux" | "pane_gone" | "unsupported_server" | "attach_failed"; message: string; }; /** * Jump to a pane, wherever it lives. * * NEVER MUTATES STATE ON FAILURE. A failure is a report: a reason and a message, * nothing written. The next collect reconciles either way, and only the owning * node can author facts about its own panes. */ declare function jumpToAgent(store: Store, agent: PaneView, mux?: Mux, run?: Runner): JumpResult; interface Channel { exec(target: string, argv: string[]): Promise; } declare const ssh: Channel; declare function hasWarmSocket(target: string): boolean; declare const MAX_CONCURRENT_PEERS = 8; /** * The optional half of a collect, as a bag rather than four positionals: the * call site was already `collect(store, ssh, now, undefined, mux)`, and a fifth * bare number next to another is how `now` and `floorMs` get swapped. */ type CollectOptions = { /** Bounds the whole run. Injectable so tests need not wait out real time. */ deadline?: Promise; /** The mux reconciliation asks which panes are alive. */ mux?: Mux; /** * Skip peers attempted within this many ms. Zero -- the default -- fetches * every peer, which is what a deliberate `murmur collect` wants. Only the * status bar and the picker's background reload, both on a timer or behind a * painted list, pass a floor. * * Opt IN: a surface that forgets it keeps always-fetch, which is merely * wasteful. The opposite default would silently serve stale data. */ floorMs?: number; /** * Source of the floor's jitter, in [0, 1). Injected for the same reason `now` * and `isAlive` are: a test pins both edges of the window by returning 0 and * a value approaching 1, rather than sampling and hoping. */ random?: () => number; /** * Whether a peer has a warm ControlMaster socket to ride. * * Injected like `random` and `now`, so a test needs no ssh binary and can * assert WHICH peers were probed -- the only way to pin the cost control in * `duePeers`, since the probe is ~20ms per host on a path that runs per tick. */ warm?: (target: string) => boolean; }; type CollectResult = { peer: string; ok: boolean; /** Panes in the snapshot we just stored. Zero is a normal, valid answer. */ panes: number; error?: string; /** * True when the peer could not be reached at all, as opposed to answering * with something wrong. * * A fleet normally has nodes asleep, so this is expected rather than a fault. * Callers stay quiet about it while still reporting reachable-but-broken -- a * bad snapshot version, a missing binary, an auth problem. */ unreachable?: boolean; }; /** * Fetch every peer's snapshot, validate it, and replace the cache whole. * * Concurrent because an unreachable peer costs the full ssh timeout and a * serial loop charged that to every peer behind it: three asleep laptops hung * `murmur status` for thirty seconds. Applied serially in peer order, since * better-sqlite3 is synchronous and a stable order keeps the results aligned * with `store.peers()`. * * One round trip per peer, never a second: the document is complete, so what * arrives either replaces the cache entirely or does not touch it. */ declare function collect(store: Store, channel: Channel, now?: number, options?: CollectOptions): Promise; /** * The one ssh a glance runs, injectable for the same reason `agents.ts` has * `Runner`: without a seam the remote branch cannot be tested without a second * machine, and the skip below -- which is the whole point of this file's * bug history -- would have no coverage at all. */ type GlanceRunner = (target: string, argv: string[]) => string; declare function glance(store: Store, agent: PaneView, lines?: number, run?: GlanceRunner): string | null; declare const ATTACH_PLACEHOLDER = "{attach}"; declare const PANE_PLACEHOLDER = "{pane}"; /** Today's remote attach command, represented as an opaque command template. */ declare function defaultJumpCommand(target: string): string; declare function tmuxAttachCommand({ server, pane }: PaneIdentity): string; /** Substitute values into a peer-owned transport without parsing or rewriting it. */ declare function renderJumpCommand(template: string, destination: PaneIdentity): string; declare function stateDir(): string; declare function configDir(): string; /** The current-state database. The only database murmur holds. */ declare function dbPath(): string; /** * A peer answered, and what it said is not a snapshot. * * A distinct type because the collector must be able to tell this from an * unreachable host: a node that serves a bad document is REACHABLE BUT BROKEN, * and an operator needs to see that rather than "asleep, probably". */ declare class SnapshotInvalidError extends Error { readonly path: string; constructor(path: string, detail: string); } /** * Parse and totally validate one snapshot document. * * `murmur_snapshot` must be exactly `SNAPSHOT_VERSION`. A higher value is rejected, and so is a * LOWER one: compatibility is offered in neither direction, because a reader * that accepted an older document would be guessing at the fields that version * added -- which is precisely the state a human is acting on. A version mismatch * is an operator-visible pairing problem, and saying so is the honest report. */ declare function parseSnapshot(input: string): Snapshot; type Counts = Record; type StatusOptions = { mux?: Mux; }; type Status = { counts: Counts; orchestrated_counts: Counts; panes: PaneView[]; peers: { name: string; /** * The ssh target, which is what a REMEDY must name. `peer add * [target]` takes them separately, so a peer called `dev` can point at * `user@box.example` -- and a suggested command built from the name would * then not run. */ target: string; display_name: string | null; fetched_at: number | null; snapshot_at: number | null; last_error: string | null; stale: boolean; /** * The operator must authenticate interactively before this peer can be * collected again: it has answered before, its last attempt was refused on * auth, and no warm ControlMaster socket exists to ride. * * Derived, never stored, which is what makes it self-correcting -- `ssh * ` creates the socket and this clears on the next read, with no * successful fetch required and no state anyone has to remember to clean up. */ needs_session: boolean; }[]; }; declare function tmuxStatus(view: Status): string; /** * The current view. Pure with respect to the network: the caller decides whether * to collect first (see `statusWithCollect`). * * `identity` is required rather than resolved here, because every caller is a * command that already fails without one. */ declare function status(store: Store, identity: NodeIdentity, now?: number, warm?: (target: string) => boolean, context?: SortContext, options?: StatusOptions): Status; /** * Collect from peers, then read. This is what every user-facing surface wants: * the view reflects the sync that just ran, rather than the one before it. * * Awaiting matters twice over. A fire-and-forget collect shows data one run * stale, and callers close the store in a `finally`, so a collect still in * flight lands on a closed handle and reports "The database connection is not * open" -- which looks like corruption rather than a race. * * Sync must never fail a command and on this path must never print either: * `status` runs on every tick and the picker's reload runs behind a popup, so * one sleeping laptop would write ssh diagnostics to stderr several times a * minute forever. `murmur collect`, run deliberately, is the only place that * prints. * * `floorMs` is how a caller says whether it is a TIMER or a PERSON. The status * bar passes COLLECT_FLOOR_MS so fetch rate stops tracking redraw rate. The * picker's rows path passes nothing: `^r` is a person asking now, and a refresh * key that skipped the fetch would be a key that silently does nothing. */ declare function statusWithCollect(store: Store, identity: NodeIdentity, now?: number, channel?: Channel, options?: CollectOptions): Promise; declare const MURMUR_VERSION: string; export { ATTACH_PLACEHOLDER, type Activity, type ActivityUpdate, type AgentClaim, type AgentMeta, type AgentRelease, type AttentionKind, type AttentionRequest, type Channel, type ClaimResult, type CollectResult, DEFAULT_DRIVER, type Driver, type Freshness, type JumpResult, type LiveCheck, type LocalPane, type LocalWorld, type Location, MAX_CONCURRENT_PEERS, type Mux, NEEDS_HUMAN, type NodeIdentity, PANE_PLACEHOLDER, type PaneId, type PaneIdentity, type PaneView, type PeerFetch, type PeerRecord, RENDER_PRIORITY, type ReconcileSummary, type RenderState, STALENESS_MS, type SessionId, type Snapshot, type SnapshotAgent, type SnapshotAttention, SnapshotInvalidError, type SnapshotPane, type Status, type Store, type TmuxServer, MURMUR_VERSION as VERSION, type WindowId, age, agentLabel, agentLocation, asPaneId, asSessionId, asWindowId, collect, configDir, createIdentity, dbPath, defaultJumpCommand, freshness, glance, hasWarmSocket, jumpToAgent, loadIdentity, openStore, paneViews, parseSnapshot, pidAlive, renderJumpCommand, renderState, setDisplayName, shellQuote, ssh, stateDir, status, statusWithCollect, tmux, tmuxAttachCommand, tmuxStatus, viewSort };