@martintrojer/murmur 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,192 +1,426 @@
1
- type AgentState = "working" | "blocked" | "done" | "crashed" | "cleared";
1
+ /**
2
+ * tmux's three id kinds, kept apart by the type system.
3
+ *
4
+ * tmux itself is unambiguous about this and prints a sigil on every id --
5
+ * `session=$25 window=@75 pane=%89` -- but they are all strings, so murmur
6
+ * could and did pass one where another was meant. Twice, in shipped code: a
7
+ * sweep keyed on window liveness deleted ten live agents, and a window cached
8
+ * at extension startup badged the window a moved pane had left.
9
+ *
10
+ * An agent is addressed by its PANE, which keeps its id across `move-pane`,
11
+ * `break-pane`, and a window closed and reopened. A session and a window are
12
+ * only where that pane currently lives, and both may differ between two reports
13
+ * from one agent. So the rule the brands enforce is:
14
+ *
15
+ * only a pane may decide whether an agent exists.
16
+ *
17
+ * Branding is a compile-time fiction: at runtime these are the same strings
18
+ * tmux printed, which is what keeps the snapshot document and every stored row
19
+ * byte-identical.
20
+ */
21
+ declare const brand: unique symbol;
22
+ /** A tmux session id, `$N`. Mutable location. */
23
+ type SessionId = string & {
24
+ readonly [brand]: "session";
25
+ };
26
+ /** A tmux window id, `@N`. Mutable location -- never an agent's identity. */
27
+ type WindowId = string & {
28
+ readonly [brand]: "window";
29
+ };
30
+ /** A tmux pane id, `%N`. The agent's identity, stable for its whole life. */
31
+ type PaneId = string & {
32
+ readonly [brand]: "pane";
33
+ };
34
+ declare function asSessionId(raw: string): SessionId;
35
+ declare function asWindowId(raw: string): WindowId;
36
+ declare function asPaneId(raw: string): PaneId;
37
+
38
+ /**
39
+ * The three independent facts, as types.
40
+ *
41
+ * `activity` is what the pane's own process says it is doing. `attention` is
42
+ * whether a human is wanted. `freshness` (src/view.ts) is how recently we
43
+ * reached the node that reported. They are three independent fields, never one
44
+ * enum, and absence carries meaning: no attention row means "nothing to see",
45
+ * no agent row means "no agent here".
46
+ */
47
+ type Activity = "running" | "stopped";
48
+ type AttentionKind = "done" | "blocked" | "crashed";
49
+ /**
50
+ * Who is waiting on this agent -- a human, or a supervisor that consumes the
51
+ * result. Not "which harness"; that is `cli`.
52
+ */
2
53
  type Driver = "human" | "orchestrated";
3
54
  declare const DEFAULT_DRIVER: Driver;
4
- type Event = {
5
- host_id: string;
6
- seq: number;
7
- ts: number;
8
- agent_id: string;
9
- session: string;
10
- window: string;
11
- pane: string;
55
+ /**
56
+ * Where a pane currently lives. Location, never identity.
57
+ *
58
+ * `pane` is the address and is stable for the life of the pane; `session` and
59
+ * `window` are only where that pane currently is, and both change under
60
+ * move-pane and break-pane. Only a pane may decide whether an agent exists,
61
+ * which is what the brands in ./ids.js enforce.
62
+ */
63
+ type Location = {
64
+ session: SessionId;
65
+ window: WindowId;
66
+ pane: PaneId;
12
67
  session_name: string | null;
13
68
  window_name: string | null;
69
+ };
70
+ /** Owner-reported metadata about the agent in a pane. */
71
+ type AgentMeta = {
14
72
  agent_name: string | null;
15
73
  pi_session: string | null;
16
74
  workstream: string | null;
17
75
  role: string | null;
18
- cli: string | null;
19
- driver: Driver | null;
20
- kind: string;
21
- state: AgentState | string;
22
- message: string;
23
- pid: number | null;
24
- synthetic: boolean;
25
- reason: string;
26
- extra: Record<string, unknown>;
76
+ cli: string;
77
+ driver: Driver;
27
78
  };
28
- type Peer = {
79
+ type PeerRecord = {
29
80
  name: string;
30
81
  target: string;
31
82
  host_id: string | null;
32
83
  display_name: string | null;
33
- watermark: number;
84
+ /** The whole validated document, or null when we have never parsed one. */
85
+ snapshot: Snapshot | null;
86
+ /** The PEER's clock: when that node built the document. */
87
+ snapshot_at: number | null;
88
+ /** OUR clock: when we last reached it. Freshness is computed from this. */
34
89
  fetched_at: number | null;
35
- /** When a jump last found this peer's tmux server down. Null once it answers. */
36
- tmux_down_at: number | null;
90
+ last_attempt_at: number | null;
91
+ last_error: string | null;
92
+ murmur_version: string | null;
93
+ /** The peer's `murmur_snapshot` value, i.e. the document version it speaks. */
94
+ snapshot_version: number | null;
37
95
  };
38
-
39
- type Location = {
40
- session: string;
41
- window: string;
42
- pane: string;
96
+ /**
97
+ * One node's whole current state. Complete, never a delta: a peer that returns
98
+ * one has said everything it knows, so absence from it is absence.
99
+ */
100
+ type Snapshot = {
101
+ murmur_snapshot: 1;
102
+ host_id: string;
103
+ display_name: string;
104
+ murmur_version: string;
105
+ generated_at: number;
106
+ panes: SnapshotPane[];
107
+ };
108
+ type SnapshotPane = {
109
+ pane: PaneId;
110
+ session: SessionId;
111
+ window: WindowId;
43
112
  session_name: string | null;
44
113
  window_name: string | null;
114
+ /** Null for an attention-only pane: valid, listable, jumpable. */
115
+ agent: SnapshotAgent | null;
116
+ attention: SnapshotAttention[];
45
117
  };
46
- interface Mux {
47
- currentWindow(): Location | null;
48
- liveWindows(): Set<string> | null;
49
- setState(window: string, state: AgentState | null): void;
50
- attach(session: string, window: string): boolean;
51
- windowNames(): Map<string, string>;
52
- windowForPane(pane: string): string | null;
53
- panesInWindow(window: string): string[];
54
- windowNamed(name: string): string | null;
55
- selectWindow(window: string): boolean;
56
- newWindow(name: string, command: string): boolean;
57
- capture(pane: string, lines?: number): string | null;
58
- }
59
- declare const tmux: Mux;
60
- declare function pidAlive(pid: number): boolean;
61
-
62
- interface Channel {
63
- exec(target: string, argv: string[]): Promise<string>;
64
- }
65
- declare const ssh: Channel;
66
- declare function hasWarmSocket(target: string): boolean;
67
-
118
+ type SnapshotAgent = AgentMeta & {
119
+ agent_id: string;
120
+ activity: Activity;
121
+ claimed_at: number;
122
+ updated_at: number;
123
+ };
124
+ type SnapshotAttention = {
125
+ kind: AttentionKind;
126
+ message: string;
127
+ source: string;
128
+ requested_at: number;
129
+ };
130
+ /**
131
+ * Whether a pid is still running. A parameter everywhere it is consulted, so a
132
+ * test needs no process table.
133
+ */
68
134
  type LiveCheck = (pid: number) => boolean;
69
- type AgentView = {
135
+ type AgentClaim = {
136
+ location: Location;
137
+ owner_pid: number;
138
+ meta: AgentMeta;
139
+ now?: number;
140
+ isAlive?: LiveCheck;
141
+ };
142
+ type ClaimResult = {
143
+ outcome: "claimed";
70
144
  agent_id: string;
71
- host_id: string;
72
- state: AgentState | null;
73
- event: Event | null;
74
- workstream: string | null;
75
- role: string | null;
76
- cli: string | null;
77
- driver: Driver;
78
- session: string;
79
- window: string;
80
- pane: string;
81
- session_name: string | null;
82
- window_name: string | null;
83
- agent_name: string | null;
84
- pi_session: string | null;
85
- fetched_at: number | null;
145
+ } | {
146
+ outcome: "retained";
147
+ agent_id: string;
148
+ } | {
149
+ outcome: "replaced";
150
+ agent_id: string;
151
+ previous_agent_id: string;
152
+ } | {
153
+ outcome: "refused";
154
+ held_by_pid: number;
86
155
  };
87
- declare function foldAgent(events: Event[], isAlive: LiveCheck): {
88
- state: AgentState | null;
89
- event: Event | null;
156
+ type ActivityUpdate = {
157
+ agent_id: string;
158
+ owner_pid: number;
159
+ activity: Activity;
160
+ location: Location;
161
+ now?: number;
162
+ };
163
+ type AgentRelease = {
164
+ agent_id: string;
165
+ owner_pid: number;
90
166
  };
91
- declare function foldAll(events: Event[], isAlive: LiveCheck): AgentView[];
92
- declare function attentionSort(views: AgentView[]): AgentView[];
93
- declare function isStale(fetchedAt: number | null, now: number, thresholdMs?: number): boolean;
94
-
95
167
  /**
96
- * Local storage shape. Bump on any change to the events or peers tables.
168
+ * Everything an attention writer may say. There is no agent_id, no owner_pid,
169
+ * no activity and no owner metadata field, and adding one is a contract change.
170
+ */
171
+ type AttentionRequest = {
172
+ kind: AttentionKind;
173
+ location: Location;
174
+ message: string;
175
+ source: string;
176
+ now?: number;
177
+ };
178
+ /**
179
+ * The only local facts reconciliation is allowed to consult.
97
180
  *
98
- * Distinct from `SCHEMA_VERSION` in export.ts, which versions the *wire*: a
99
- * node can change how it stores events without changing what it sends, and a
100
- * wire change should not throw away local history.
181
+ * `panes` is null when tmux could not answer, which is not evidence of death.
182
+ * `isAlive` and `now` are parameters so a test needs no process table and no
183
+ * clock control.
101
184
  */
102
- declare const STORE_VERSION = 2;
103
- type NewEvent = Omit<Event, "host_id" | "seq" | "ts" | "session_name" | "window_name" | "agent_name" | "pi_session"> & {
104
- ts?: number;
105
- session_name?: string | null;
106
- window_name?: string | null;
107
- agent_name?: string | null;
108
- pi_session?: string | null;
185
+ type LocalWorld = {
186
+ panes: Set<PaneId> | null;
187
+ isAlive?: LiveCheck;
188
+ now?: number;
189
+ };
190
+ type ReconcileSummary = {
191
+ crashed: PaneId[];
192
+ removed: PaneId[];
193
+ attention_removed: PaneId[];
194
+ };
195
+ type PeerFetch = {
196
+ ok: true;
197
+ snapshot: Snapshot;
198
+ at: number;
199
+ } | {
200
+ ok: false;
201
+ error: string;
202
+ at: number;
109
203
  };
204
+
205
+ type NodeIdentity = {
206
+ host_id: string;
207
+ display_name: string;
208
+ };
209
+ /**
210
+ * This node's identity, or null when it has none.
211
+ *
212
+ * A READ, and only a read: nothing mints here. Every command that needs a
213
+ * host_id fails with "murmur is not initialised on this node; run: murmur init"
214
+ * rather than bringing a node into existence as a side effect of a status-bar
215
+ * tick.
216
+ */
217
+ declare function loadIdentity(): NodeIdentity | null;
218
+ /** Create this node's identity. Only `murmur init` calls it. */
219
+ declare function createIdentity(displayName?: string): NodeIdentity;
220
+ /**
221
+ * Rename an existing node, keeping its `host_id`.
222
+ *
223
+ * `murmur init --name` on an already-initialised node used to ignore the flag
224
+ * silently, which is the one thing a rename must not do.
225
+ */
226
+ declare function setDisplayName(displayName: string): NodeIdentity;
227
+
228
+ /**
229
+ * The store, and the only place in murmur that holds a database handle or
230
+ * writes SQL.
231
+ *
232
+ * This interface is CLOSED. There is no `append`, no `ingest`, no log read, no
233
+ * partial-row update, and no local read other than `localPanes` — each of those
234
+ * shapes let a writer say something it had no standing to say, and each cost a
235
+ * shipped bug. Attention methods take no agent identity at all, which is what
236
+ * makes "a notifier cannot corrupt an agent row" structural.
237
+ */
110
238
  interface Store {
111
- append(event: NewEvent): Event;
112
- ingest(events: Event[]): number;
113
- eventsSince(hostId: string, seq: number): Event[];
114
- allEvents(): Event[];
115
- /**
116
- * The most recent event for one agent, or null.
117
- *
118
- * Exists so the `clear` hook does not have to open its own SQLite handle and
119
- * write its own `ORDER BY seq DESC LIMIT 1`, which is what it used to do --
120
- * making "store is the only module touching SQL" false, and putting knowledge
121
- * of agent_id construction and event ordering in a CLI file where a schema
122
- * change would miss it. That path swallows its own errors, so the miss would
123
- * have been silent.
124
- */
125
- latestForAgent(hostId: string, agentId: string): Event | null;
126
- maxSeq(hostId: string): number;
127
- prune(horizonMs?: number): number;
128
- peers(): Peer[];
129
- /**
130
- * Drop every event for one agent from this node's replica.
131
- *
132
- * For a remote agent this is a replica eviction, not a claim about truth: the
133
- * authoring node still owns it, and a collect re-reads from the watermark if
134
- * it is still alive.
135
- */
136
- forgetAgent(agentId: string): number;
137
- forgetHost(hostId: string): number;
138
- upsertPeer(peer: Partial<Peer> & {
139
- name: string;
140
- target: string;
141
- }): void;
239
+ claimAgent(claim: AgentClaim): ClaimResult;
240
+ setActivity(update: ActivityUpdate): boolean;
241
+ releaseAgent(release: AgentRelease): boolean;
242
+ requestAttention(request: AttentionRequest): void;
243
+ acknowledgePane(pane: PaneId): number;
244
+ /** The one local read. Joins agents and attention by pane. No reconciliation. */
245
+ localPanes(): SnapshotPane[];
246
+ reconcileLocal(world: LocalWorld): ReconcileSummary;
247
+ buildLocalSnapshot(identity: NodeIdentity, world: LocalWorld): Snapshot;
248
+ peers(): PeerRecord[];
249
+ addPeer(name: string, target: string): void;
142
250
  removePeer(name: string): boolean;
251
+ replacePeerSnapshot(name: string, fetch: PeerFetch): void;
143
252
  close(): void;
144
253
  }
254
+ /**
255
+ * Open the store. Takes no arguments and mints no identity.
256
+ *
257
+ * `openStore` deliberately does NOT read or create `identity.json`: identity is
258
+ * created only by `murmur init`, so a read path — a status-bar tick, a focus
259
+ * hook — cannot bring a node into existence as a side effect.
260
+ */
145
261
  declare function openStore(): Store;
146
262
 
147
- type StatusState = "working" | "blocked" | "done" | "crashed" | "idle";
148
- type Counts = Record<StatusState, number>;
149
- type Status = {
150
- counts: Counts;
151
- orchestrated_counts: Counts;
152
- agents: (AgentView & {
153
- stale: boolean;
154
- age_ms: number | null;
155
- event_age_ms: number | null;
156
- tmux_down: boolean;
157
- host: string;
158
- })[];
159
- peers: {
160
- name: string;
161
- display_name: string | null;
162
- fetched_at: number | null;
163
- stale: boolean;
164
- }[];
263
+ type Freshness = "fresh" | "stale";
264
+ /**
265
+ * What a surface paints. Presentation only, derived from the three independent
266
+ * facts and never stored.
267
+ */
268
+ type RenderState = "crashed" | "blocked" | "done" | "running" | "idle";
269
+ /**
270
+ * THE single ordering table: which state matters most, for sorting and for
271
+ * choosing one word to show.
272
+ *
273
+ * `status.ts` and `pick.ts` import this rather than declaring their own copies,
274
+ * so no two surfaces can sort one list differently.
275
+ */
276
+ declare const RENDER_PRIORITY: readonly RenderState[];
277
+ /**
278
+ * The attention kinds only a human can answer, and the second table both
279
+ * surfaces must agree on.
280
+ *
281
+ * `blocked` means waiting for an answer an orchestrator cannot give -- mu places
282
+ * work, it cannot choose between two approaches. `crashed` means the process
283
+ * died, which a supervisor may or may not retry. Everything else about an
284
+ * orchestrated agent is its supervisor's business.
285
+ *
286
+ * `pick.ts` uses it to decide which crew rows are visible by default and
287
+ * `status.ts` to decide which crew states reach the status bar. They were two
288
+ * literals in two files answering one question, which is how a row that needed a
289
+ * human became one a human could not see.
290
+ */
291
+ declare const NEEDS_HUMAN: readonly AttentionKind[];
292
+ /**
293
+ * One pane, as every surface reads it: address, the three independent facts,
294
+ * owner metadata, and ages.
295
+ *
296
+ * Local and remote panes are the same type, built by the same mapping, because
297
+ * `Store.localPanes()` and a peer's cached snapshot both return
298
+ * `SnapshotPane[]`. One mapping means local and remote cannot drift apart.
299
+ */
300
+ type PaneView = {
301
+ host_id: string;
302
+ /** The name the operator typed, or this node's display_name. */
303
+ host: string;
304
+ local: boolean;
305
+ pane: PaneId;
306
+ session: SessionId;
307
+ window: WindowId;
308
+ session_name: string | null;
309
+ window_name: string | null;
310
+ /** Null for an attention-only pane, which has no agent row. */
311
+ activity: Activity | null;
312
+ attention: AttentionKind[];
313
+ freshness: Freshness;
314
+ agent_id: string | null;
315
+ agent_name: string | null;
316
+ pi_session: string | null;
317
+ workstream: string | null;
318
+ role: string | null;
319
+ cli: string | null;
320
+ driver: Driver;
321
+ /** When the pane's own node last said something. Never `fetched_at`. */
322
+ updated_at: number | null;
323
+ /** When that node generated its snapshot. Null for local. */
324
+ snapshot_at: number | null;
325
+ /** When we last reached that node. Null for local. */
326
+ fetched_at: number | null;
165
327
  };
166
328
  /**
167
- * Fold the current view. Pure with respect to the network: the caller decides
168
- * whether to collect first (see `statusWithCollect`).
329
+ * How long a peer may go unfetched before its panes render stale.
330
+ *
331
+ * Re-exported from here rather than imported from the collector by view
332
+ * consumers, so freshness has one definition. See collector.ts for why sixty
333
+ * seconds.
334
+ */
335
+ declare const STALENESS_MS = 60000;
336
+ /**
337
+ * A duration as the shortest thing worth reading: "5m", "2h", "3d".
338
+ *
339
+ * Under a minute is the empty string: an age that changes every second is noise
340
+ * in a status column. This and `freshness` are the only two places a duration
341
+ * becomes text or a verdict.
342
+ */
343
+ declare function age(ms: number | null): string;
344
+ /**
345
+ * Freshness of a NODE, never of an agent.
346
+ *
347
+ * A peer we have never reached is stale rather than fresh: null means the first
348
+ * collect has not succeeded yet, and an unreachable host you just added must not
349
+ * render as up to date.
350
+ */
351
+ declare function freshness(fetchedAt: number | null, now: number, thresholdMs?: number): Freshness;
352
+ /**
353
+ * One word for a pane. Attention wins over activity, because attention is a
354
+ * request and activity is a description.
355
+ *
356
+ * A running agent with `blocked` attention is a valid and expected state, and
357
+ * surfaces that can show both, do — this is only for the ones that must pick.
169
358
  */
170
- declare function status(store: Store, now?: number): Status;
359
+ declare function renderState(view: Pick<PaneView, "activity" | "attention">): RenderState;
360
+ /**
361
+ * Every pane this node knows about: its own, plus one cached snapshot per peer.
362
+ *
363
+ * `identity` is non-null because every caller is a command that already requires
364
+ * `murmur init`, so no pane can be misclassified as remote by an absent one.
365
+ *
366
+ * No liveness is probed here, for local or remote. A remote pane's `activity` is
367
+ * whatever its own node last said; a stale node keeps its last-known fields
368
+ * verbatim beside an explicit warning.
369
+ */
370
+ declare function paneViews(store: Store, identity: NodeIdentity, now?: number): PaneView[];
371
+ /**
372
+ * Attention-first ordering, then the newest news, then address.
373
+ *
374
+ * TOTAL on purpose, and that is the whole reason the last two comparisons
375
+ * exist. Ties on state and age are ordinary rather than exotic -- a pair of
376
+ * crashed panes reconciled in one transaction shares a `requested_at` exactly --
377
+ * and `Array.prototype.sort` is stable only with respect to the order it was
378
+ * GIVEN, which here is whatever SQLite and the peer loop happened to produce. An
379
+ * unbroken tie therefore makes the list depend on that order: a status bar
380
+ * reshuffles between two identical ticks, and a picker row moves under the
381
+ * keypress that was aimed at it.
382
+ *
383
+ * Presentation only. No caller may read meaning into the position of a row --
384
+ * pane order in a snapshot carries none either, so a reader sorts for itself
385
+ * rather than trusting what it was served.
386
+ */
387
+ declare function viewSort(views: PaneView[]): PaneView[];
388
+
389
+ interface Mux {
390
+ currentWindow(): Location | null;
391
+ livePanes(): Set<PaneId> | null;
392
+ setWindowBadge(window: WindowId, state: RenderState | null): void;
393
+ attach(session: SessionId, window: WindowId): boolean;
394
+ windowForPane(pane: PaneId): WindowId | null;
395
+ panesInWindow(window: WindowId): PaneId[];
396
+ capture(pane: PaneId, lines?: number): string | null;
397
+ clientName(): string | null;
398
+ currentTarget(): string | null;
399
+ sessionNamed(name: string): boolean;
400
+ newSession(name: string, command: string): boolean;
401
+ setSessionOption(session: string, option: string, value: string): void;
402
+ switchClient(client: string | null, session: string): boolean;
403
+ }
404
+ declare const tmux: Mux;
405
+ declare function pidAlive(pid: number): boolean;
171
406
 
172
- type Agent = Status["agents"][number];
173
407
  /**
174
- * The most specific human-readable name an agent has, never a tmux id.
408
+ * The most specific human-readable name a pane's agent has, never a tmux id.
175
409
  *
176
- * Four sources, most to least specific: mu's agent name, pi's session name,
177
- * the tmux window name, the tmux session name. The old picker showed window
178
- * names and that was the thing it did better than raw `$26:@79`; these are all
179
- * recorded on the event, so this reads the same for a local and a remote agent.
410
+ * Four sources, most to least specific: mu's agent name, pi's session name, the
411
+ * tmux window name, the tmux session name. All are recorded by the node that
412
+ * owns the pane, so this reads the same for a local and a remote pane -- a
413
+ * reader cannot resolve a remote window id against its own tmux.
180
414
  *
181
415
  * Falls back to the window id only when a node recorded no names at all, which
182
- * means a pre-names event or a non-tmux harness.
416
+ * means a non-tmux harness.
183
417
  */
184
- declare function agentLabel(agent: Agent): string;
418
+ declare function agentLabel(agent: PaneView): string;
185
419
  /**
186
- * Where the agent lives, for the second column. Names only -- the ids are what
420
+ * Where the pane lives, for the second column. Names only -- the ids are what
187
421
  * jumps, not what a human reads.
188
422
  */
189
- declare function agentLocation(agent: Agent): string;
423
+ declare function agentLocation(agent: PaneView): string;
190
424
  declare function shellQuote(value: string): string;
191
425
  /**
192
426
  * The one process call jump makes that is not a tmux command: the remote probe,
@@ -204,67 +438,125 @@ type JumpResult = {
204
438
  ok: true;
205
439
  } | {
206
440
  ok: false;
207
- reason: "no_peer" | "unreachable" | "no_tmux" | "window_gone" | "attach_failed";
441
+ reason: "no_peer" | "unreachable" | "no_tmux" | "pane_gone" | "attach_failed";
208
442
  message: string;
209
443
  };
210
- declare function jumpToAgent(store: Store, agent: Agent, mux?: Mux, run?: Runner): JumpResult;
211
-
212
444
  /**
213
- * How long a peer may go unfetched before it renders stale.
214
- *
215
- * Not derived from a collect interval, because murmur has no scheduler: there
216
- * is no timer here, and `collect` runs only when a command asks for it. In
217
- * practice the cadence is the operator's tmux `status-interval`, since
218
- * `murmur status` collects and tmux re-runs it on a tick.
219
- *
220
- * So this is a judgement about the operator's setup, not arithmetic on a
221
- * constant murmur controls. Sixty seconds is comfortably above a default 15s
222
- * status bar -- a peer needs to miss several ticks before it is called out,
223
- * which keeps one slow fetch from flickering the HUD. A status bar slower than
224
- * this will show every peer permanently stale; that is the number to change if
225
- * so.
445
+ * Jump to a pane, wherever it lives.
446
+ *
447
+ * NEVER MUTATES STATE ON FAILURE. A failure is a report: a reason and a message,
448
+ * nothing written. The next collect reconciles either way, and only the owning
449
+ * node can author facts about its own panes.
226
450
  */
227
- declare const STALENESS_MS = 60000;
451
+ declare function jumpToAgent(store: Store, agent: PaneView, mux?: Mux, run?: Runner): JumpResult;
452
+
453
+ interface Channel {
454
+ exec(target: string, argv: string[]): Promise<string>;
455
+ }
456
+ declare const ssh: Channel;
457
+ declare function hasWarmSocket(target: string): boolean;
458
+
228
459
  declare const MAX_CONCURRENT_PEERS = 8;
229
460
  type CollectResult = {
230
461
  peer: string;
231
462
  ok: boolean;
232
- ingested: number;
463
+ /** Panes in the snapshot we just stored. Zero is a normal, valid answer. */
464
+ panes: number;
233
465
  error?: string;
466
+ /**
467
+ * True when the peer could not be reached at all, as opposed to answering
468
+ * with something wrong.
469
+ *
470
+ * A fleet normally has nodes that are asleep or switched off, so this is the
471
+ * expected outcome rather than a fault, and callers use it to stay quiet
472
+ * about the ordinary case while still reporting a peer that is reachable but
473
+ * broken -- a bad snapshot version, a missing binary, an auth problem.
474
+ */
475
+ unreachable?: boolean;
234
476
  };
235
477
  /**
236
- * Peers are fetched concurrently and applied serially.
478
+ * Fetch every peer's snapshot, validate it, and replace the cache whole.
237
479
  *
238
480
  * Concurrent because an unreachable peer costs the full ssh timeout, and a
239
- * serial loop charged that to every other peer behind it: three asleep laptops
240
- * made `murmur status` hang for thirty seconds and let the HUD tick overlap
241
- * itself. Fanning out makes the whole collect cost the slowest peer, not the
242
- * sum capped at MAX_CONCURRENT_PEERS in flight and bounded overall by
243
- * COLLECT_DEADLINE_MS.
244
- *
245
- * Applied serially, in peer order, because better-sqlite3 is synchronous: there
246
- * is nothing to win by interleaving writes, and keeping the order stable keeps
247
- * the result list aligned with `store.peers()`.
481
+ * serial loop charged that to every peer behind it: three asleep laptops made
482
+ * `murmur status` hang for thirty seconds. Applied serially in peer order,
483
+ * because better-sqlite3 is synchronous and a stable order keeps the result list
484
+ * aligned with `store.peers()`.
485
+ *
486
+ * One round trip per peer, and never a second: the document is complete, so what
487
+ * arrives either replaces the cache entirely or does not touch it.
248
488
  */
249
489
  declare function collect(store: Store, channel: Channel, now?: number, deadline?: Promise<void>): Promise<CollectResult[]>;
250
490
 
251
- declare const SCHEMA_VERSION = 2;
252
- declare function eventFromWire(wire: Record<string, unknown>): Event;
253
- declare function exportJsonl(store: Store, since: number, isAlive: LiveCheck, live?: Set<string> | null): string;
254
-
255
- declare function glance(store: Store, agent: Agent, lines?: number): string | null;
256
-
257
- type NodeIdentity = {
258
- host_id: string;
259
- display_name: string;
260
- };
261
- declare function loadIdentity(): NodeIdentity | null;
262
- declare function ensureIdentity(displayName?: string): NodeIdentity;
491
+ declare function glance(store: Store, agent: PaneView, lines?: number): string | null;
263
492
 
264
493
  declare function stateDir(): string;
265
494
  declare function configDir(): string;
495
+ /** The current-state database. The only database murmur holds. */
266
496
  declare function dbPath(): string;
267
497
 
268
- declare const VERSION: string;
498
+ /**
499
+ * A peer answered, and what it said is not a snapshot.
500
+ *
501
+ * A distinct type because the collector must be able to tell this from an
502
+ * unreachable host: a node that serves a bad document is REACHABLE BUT BROKEN,
503
+ * and an operator needs to see that rather than "asleep, probably".
504
+ */
505
+ declare class SnapshotInvalidError extends Error {
506
+ readonly path: string;
507
+ constructor(path: string, detail: string);
508
+ }
509
+ /**
510
+ * Parse and totally validate one snapshot document.
511
+ *
512
+ * `murmur_snapshot` must be exactly 1: a higher value is rejected too, because
513
+ * forward compatibility is not offered here and a version mismatch is an
514
+ * operator-visible pairing problem. Saying so is the honest report; guessing at
515
+ * a newer document's meaning is not.
516
+ */
517
+ declare function parseSnapshot(input: string): Snapshot;
518
+
519
+ type Counts = Record<RenderState, number>;
520
+ type Status = {
521
+ counts: Counts;
522
+ orchestrated_counts: Counts;
523
+ panes: PaneView[];
524
+ peers: {
525
+ name: string;
526
+ display_name: string | null;
527
+ fetched_at: number | null;
528
+ snapshot_at: number | null;
529
+ last_error: string | null;
530
+ stale: boolean;
531
+ }[];
532
+ };
533
+ declare function tmuxStatus(view: Status): string;
534
+ /**
535
+ * The current view. Pure with respect to the network: the caller decides whether
536
+ * to collect first (see `statusWithCollect`).
537
+ *
538
+ * `identity` is required rather than resolved here, because every caller is a
539
+ * command that already fails without one.
540
+ */
541
+ declare function status(store: Store, identity: NodeIdentity, now?: number): Status;
542
+ /**
543
+ * Collect from peers, then read. This is what every user-facing surface wants:
544
+ * the view reflects the sync that just ran, rather than the one before it.
545
+ *
546
+ * Awaiting matters for two reasons. A fire-and-forget collect makes every
547
+ * invocation show data one run stale. And the callers close the store in a
548
+ * `finally`, so a collect still in flight lands on a closed handle and reports
549
+ * "The database connection is not open", which looks like corruption rather
550
+ * than a race.
551
+ *
552
+ * Sync must never fail a command, and on this path it must never print either:
553
+ * `status` runs on every status-bar tick and `pick` runs inside a
554
+ * display-popup, so one sleeping laptop would otherwise write ssh diagnostics
555
+ * to stderr several times a minute, forever. `murmur collect`, which a human
556
+ * runs deliberately, is the only place that prints.
557
+ */
558
+ declare function statusWithCollect(store: Store, identity: NodeIdentity, now?: number, channel?: Channel): Promise<Status>;
559
+
560
+ declare const MURMUR_VERSION: string;
269
561
 
270
- export { type Agent, type AgentState, type AgentView, type Channel, type CollectResult, DEFAULT_DRIVER, type Driver, type Event, type JumpResult, type LiveCheck, MAX_CONCURRENT_PEERS, type Mux, type NewEvent, type NodeIdentity, type Peer, SCHEMA_VERSION, STALENESS_MS, STORE_VERSION, type Status, type Store, VERSION, agentLabel, agentLocation, attentionSort, collect, configDir, dbPath, ensureIdentity, eventFromWire, exportJsonl, foldAgent, foldAll, glance, hasWarmSocket, isStale, jumpToAgent, loadIdentity, openStore, pidAlive, shellQuote, ssh, stateDir, status, tmux };
562
+ export { 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 LocalWorld, type Location, MAX_CONCURRENT_PEERS, type Mux, NEEDS_HUMAN, type NodeIdentity, type PaneId, 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, MURMUR_VERSION as VERSION, type WindowId, age, agentLabel, agentLocation, asPaneId, asSessionId, asWindowId, collect, configDir, createIdentity, dbPath, freshness, glance, hasWarmSocket, jumpToAgent, loadIdentity, openStore, paneViews, parseSnapshot, pidAlive, renderState, setDisplayName, shellQuote, ssh, stateDir, status, statusWithCollect, tmux, tmuxStatus, viewSort };