@martintrojer/murmur 0.1.4 → 0.2.1
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/ARCHITECTURE.md +740 -225
- package/CHANGELOG.md +119 -0
- package/README.md +136 -27
- package/dist/cli.js +1312 -839
- package/dist/cli.js.map +1 -1
- package/dist/extension/murmur-pi.js +196 -110
- package/dist/extension/murmur-pi.js.map +1 -1
- package/dist/extension/store.js +413 -198
- package/dist/extension/store.js.map +1 -1
- package/dist/index.d.ts +480 -188
- package/dist/index.js +856 -611
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,192 +1,426 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
19
|
-
driver: Driver
|
|
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
|
|
79
|
+
type PeerRecord = {
|
|
29
80
|
name: string;
|
|
30
81
|
target: string;
|
|
31
82
|
host_id: string | null;
|
|
32
83
|
display_name: string | null;
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
*
|
|
168
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
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
|
|
416
|
+
* means a non-tmux harness.
|
|
183
417
|
*/
|
|
184
|
-
declare function agentLabel(agent:
|
|
418
|
+
declare function agentLabel(agent: PaneView): string;
|
|
185
419
|
/**
|
|
186
|
-
* Where the
|
|
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:
|
|
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" | "
|
|
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
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
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
|
-
declare function collect(store: Store, channel: Channel, now?: number, deadline?: Promise<void
|
|
250
|
-
|
|
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;
|
|
489
|
+
declare function collect(store: Store, channel: Channel, now?: number, deadline?: Promise<void>, mux?: Mux): Promise<CollectResult[]>;
|
|
254
490
|
|
|
255
|
-
declare function glance(store: Store, agent:
|
|
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
|
-
|
|
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, mux?: Mux): Promise<Status>;
|
|
559
|
+
|
|
560
|
+
declare const MURMUR_VERSION: string;
|
|
269
561
|
|
|
270
|
-
export { type
|
|
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 };
|