@martintrojer/murmur 0.1.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.
@@ -0,0 +1,357 @@
1
+ # murmur architecture
2
+
3
+ How it works, why it is built this way, and why it exists rather than a
4
+ configuration of something else.
5
+
6
+ ## The problem
7
+
8
+ Coding agents run on more than one machine: a laptop, a desktop, a remote box.
9
+ Each machine knows what its own agents are doing. No machine knows what the
10
+ others are doing. So "is anything blocked on me right now" means visiting each
11
+ one in turn, and jumping to an agent means first remembering which host it lives
12
+ on.
13
+
14
+ That is the whole problem. Everything below is in service of it, and the design
15
+ spends most of its effort refusing to solve harder problems nearby.
16
+
17
+ ## The shape
18
+
19
+ ```
20
+ pi agent ──in-process── murmur store ── events.db (one per node)
21
+
22
+ ssh murmur export --since N
23
+
24
+ collector ── fold ── picker
25
+
26
+ jump: local switch, or ssh -t
27
+ ```
28
+
29
+ Each node keeps an append-only SQLite log describing only its own agents. Any
30
+ node pulls its peers' logs over ssh and folds the union into one
31
+ attention-sorted picker. No daemon, no listening socket, no master node.
32
+
33
+ murmur observes and connects. It does not place work. That is an orchestrator's
34
+ job, and mixing the two is how you end up owning scheduling, credentials and
35
+ artifact movement.
36
+
37
+ ## Why this is not a distributed system
38
+
39
+ It is single-writer-per-partition replication. Each node authors only events
40
+ about its own agents, so no two nodes ever write about the same thing. Merging
41
+ is a UNION.
42
+
43
+ Calling it "distributed" invites machinery the problem does not have: consensus,
44
+ conflict resolution, vector clocks, leader election. If a change starts to need
45
+ conflict resolution, that is the signal the single-writer invariant has been
46
+ broken somewhere.
47
+
48
+ Two consequences worth stating, because both are easy to violate by accident:
49
+
50
+ - **A node may not author events about another node's agents.** Not even
51
+ corrections. A reader that learns something (a jump proving a window is gone)
52
+ records it as *reader state*, or deletes its replica. It does not write an
53
+ event.
54
+ - **`host_id` is the origin, watermarks are keyed by peer.** These are the same
55
+ thing today, and differ the moment anything gossips. Free to preserve now,
56
+ expensive to retrofit.
57
+
58
+ ## The six units
59
+
60
+ | Unit | Does | Depends on |
61
+ | ---- | ---- | ---------- |
62
+ | `identity` | Read/create this node's `{host_id, display_name}` | state dir |
63
+ | `store` | Append, query, ingest, prune. **The only module touching SQL** | `identity` |
64
+ | `fold` | **Pure.** Events in, agent states out | nothing |
65
+ | `channel` | Seam: `exec(target, argv) -> stdout`. One impl: ssh | OS |
66
+ | `collector` | Pull each peer, ingest, advance watermark | `channel`, `store` |
67
+ | `mux` | Seam: window/pane queries, set state, attach. One impl: tmux | OS |
68
+
69
+ `fold` being pure and `store` being the only SQL is the boundary that carries
70
+ the design. Four heuristics live in one of those two modules: attention
71
+ ordering, staleness, crash synthesis, and retention. Both modules are testable
72
+ without a machine, a network or a multiplexer.
73
+
74
+ Three seams exist with exactly one implementation each: `channel` (ssh),
75
+ `harness` (pi, in-process), and `mux` (tmux). Defined so a second
76
+ backend is possible; not designed for one that does not exist.
77
+
78
+ ## The data model
79
+
80
+ One append-only table. Sole author per `host_id`. Primary key `(host_id, seq)`,
81
+ which is what makes ingest idempotent. A re-read after a partial failure is
82
+ free.
83
+
84
+ | Column | Notes |
85
+ | ------ | ----- |
86
+ | `host_id` | UUID of the **origin** node, not the node we fetched from |
87
+ | `seq` | Monotonic per `host_id`. The watermark unit |
88
+ | `ts` | Wall clock. Display ordering only |
89
+ | `agent_id` | Stable for the agent's life. Identity, distinct from location |
90
+ | `session`, `window`, `pane` | Current **location**. May change |
91
+ | `session_name`, `window_name` | Recorded by the author; ids are machine-local |
92
+ | `agent_name`, `pi_session` | Richer than tmux, and not derivable from it |
93
+ | `workstream`, `role`, `cli` | Nullable. Grouping and display |
94
+ | `driver` | `human \| orchestrated`. **Per agent, not per node** |
95
+ | `kind` | `state` today. Discriminator for future kinds |
96
+ | `state` | `working \| blocked \| done \| crashed \| cleared` |
97
+ | `message`, `pid`, `synthetic`, `reason` | Detail |
98
+ | `extra` | JSON. Unknown fields, preserved verbatim |
99
+
100
+ **Unknown data round-trips.** A node ingesting an unknown `kind` or unknown
101
+ fields stores them in `extra` and re-exports them unchanged. Without this, an
102
+ old node sitting in a future replication path silently truncates data. That
103
+ failure is invisible from both ends, and only reachable against a version you
104
+ do not control.
105
+
106
+ Deliberately absent: a generic `put`/`del` op-log (that needs conflict
107
+ resolution, which single-writer partitioning lets us skip, and it would simply
108
+ *be* `mu`), task/DAG structure (observing a task graph means owning it), and any
109
+ hybrid logical clock (nothing depends on cross-node causality; clock skew
110
+ affects display order only).
111
+
112
+ **An event log, not a state snapshot.** Current state is a fold over the log,
113
+ which buys three things: there is no state table that can disagree with the log,
114
+ incremental sync is a watermark ("everything after N", idempotent), and history
115
+ is not optional, because the picker's preview shows recent events, so the
116
+ protocol has to ship events rather than derived state.
117
+
118
+ **TypeScript, on install-story grounds.** pi is itself an npm package, so every
119
+ node that runs agents necessarily has Node and npm, so npm is not a new
120
+ dependency on exactly the machines that matter. Python would mean inventing a cross-machine
121
+ install story for the ecosystem that handles it worst, on machines where the
122
+ system interpreter is least yours to touch. The "keep the working script"
123
+ argument is weaker than it looks: those lines worked because dotfiles symlinked
124
+ them, so the moment a second machine needs them they must be packaged from
125
+ scratch either way.
126
+
127
+ ## Three ideas to understand before changing anything
128
+
129
+ Everything else is mechanical. These three are the ones to understand before
130
+ changing anything.
131
+
132
+ ### 1. State and freshness are different axes, and freshness is two
133
+
134
+ `stale` is not an agent state. The enum stays `working | blocked | done |
135
+ crashed | cleared`.
136
+
137
+ - **state** — what the agent is doing. Authored by the agent, in the log.
138
+ - **freshness** — how current our replica is. Known only by the reader.
139
+
140
+ Putting `stale` in the enum produces unanswerable questions: is a crashed agent
141
+ on an unreachable host `crashed` or `stale`? Both, on different axes.
142
+
143
+ Freshness then splits again, and missing this shipped a bug:
144
+
145
+ | | asks | answers |
146
+ | --- | --- | --- |
147
+ | `fetched_at` | how current is my copy | is the host reachable |
148
+ | event `ts` | how old is this agent's news | is the row worth believing |
149
+
150
+ A peer polled one second ago can be serving three-hour-old events. Collapsing
151
+ these made a dead host's agents render as live, because the replica really was
152
+ current.
153
+
154
+ ### 2. Facts only the author can know are recorded by the author
155
+
156
+ Pids, window liveness and window names mean something only on the machine that
157
+ owns them. So crash synthesis runs on the authoring node during export, not on
158
+ the reader.
159
+
160
+ Do it on the reader and every remote `working` agent is marked `crashed`, which
161
+ looks exactly like a real crash, so it does not get investigated. This is the
162
+ single easiest thing in the codebase to get backwards.
163
+
164
+ The same rule gave names their home: window ids are machine-local, so resolving
165
+ a remote id against the local tmux labels an agent with whatever *this* machine
166
+ has at that id. Names travel on the event instead.
167
+
168
+ ### 3. The single-machine case is the same code path
169
+
170
+ murmur replaced a 1500-line script that was the daily local tool. Zero peers is
171
+ therefore the common case:
172
+
173
+ - the pi extension appends events and sets the tmux window option
174
+ - the status bar and picker read the fold
175
+ - the collector iterates the peer list, finds nothing, and does nothing
176
+
177
+ No network, no ssh, no daemon, no added latency. Federation is strictly
178
+ additive: a `host_id` on rows that all say "me", and a loop over an empty array.
179
+
180
+ This is a constraint, not an observation. A tool that only pays for itself at
181
+ three nodes charges rent daily for fleet capability used occasionally, which is
182
+ one of the things herdr was rejected for. Measured first paint with zero peers:
183
+ 48 ms against 250 ms for the picker it replaces.
184
+
185
+ ## Design choices, and what they cost
186
+
187
+ **Pull, not push.** "Adhoc ssh" and "an open tunnel" are one model at two
188
+ latencies. Both are the reader pulling, and OpenSSH `ControlMaster` collapses
189
+ them, because the persisted control socket *is* the tunnel. Push needs a
190
+ listener, which is the thing this design does not have.
191
+
192
+ **The collector never initiates authentication.** A machine that wants a
193
+ hardware-token touch per connection makes a background collector intolerable, so
194
+ the collector rides an existing warm control socket and fails fast otherwise.
195
+ The consequence is correct: remote visibility is a side effect of having worked
196
+ on that box, and a cold host shows stale until you connect for any reason.
197
+
198
+ **Membership is local and asymmetric.** No shared node list, no registry, no
199
+ join protocol. Reachability is not symmetric: a laptop reaches a server, and
200
+ the server does not reach a laptop behind NAT that sleeps. A global list would
201
+ advertise peers half the fleet cannot use. And nothing needs one: only a node
202
+ rendering a picker needs targets, and only ones it can reach. Identity is
203
+ *discovered*. Config holds an ssh target, and the first export returns the
204
+ node's UUID and display name.
205
+
206
+ **Zero knobs.** Every setting exported to the user is one they can get wrong
207
+ invisibly. Only two are irreducible: `peers` (only the operator knows their
208
+ fleet) and `theme`. Retention horizon, collection interval and the staleness
209
+ threshold are constants or derived. The trade is real: a wrong constant needs a
210
+ release rather than an edit. Since heuristics replace knobs, the heuristics are
211
+ where the tests go.
212
+
213
+ **`driver` distinguishes who is waiting.** An agent you are talking to and an
214
+ agent an orchestrator placed want opposite treatment: when the orchestrated one
215
+ finishes, its supervisor consumes the result and nobody needs to acknowledge
216
+ anything. Same state, opposite attention. It is per *agent*, not per node,
217
+ because the normal case is one machine running your session and six spawned
218
+ workers at once. Null reads as `human`, so an older node's events degrade toward
219
+ visible.
220
+
221
+ **Glance, not remote rendering.** "Render any pane from the master" hides two
222
+ very different problems: a stateless `capture-pane` (cheap, and what the picker
223
+ preview does) and continuous frame streaming with resize negotiation and input
224
+ routing (most of herdr's codebase). Glance plus jump gets everything except
225
+ never leaving the local frame. Since you are jumping there to work anyway, that
226
+ may not be missed. This deferral is the main reason murmur is small.
227
+
228
+ ## Why not something else
229
+
230
+ Investigated against herdr 0.8.2, a T3 Code checkout, and `mu`, all built and
231
+ run locally rather than judged from their READMEs.
232
+
233
+ | | multi-machine view | pi support | state source |
234
+ | --- | --- | --- | --- |
235
+ | herdr | no — 1:1, planned, blocked | yes | screen-scraped |
236
+ | T3 Code | no — "unbuilt" by its own docs | no — 14-method adapter | driven |
237
+ | mu | state sync yes, agents no | yes | reported |
238
+ | **murmur** | **yes** | **yes, in-process** | **pushed** |
239
+
240
+ ### herdr
241
+
242
+ A Rust terminal multiplexer built for coding agents: workspaces, tabs, panes, a
243
+ per-pane `idle/working/blocked/done` sidebar, a socket API. Evaluated as a tmux
244
+ replacement and rejected on its own terms.
245
+
246
+ On multi-machine it is strictly 1:1. `--remote <target>` takes a single
247
+ target, and no agent/pane/notification subcommand has a `--host` flag anywhere.
248
+ `--remote` *replaces* the view rather than adding to it, so two machines means
249
+ two sessions and a full switch between them. Multi-client is the maintainer's
250
+ stated top priority but gated behind a server/client refactor that has been in
251
+ progress for some time.
252
+
253
+ Waiting would not help, for two structural reasons. The scope is one client
254
+ attaching to multiple *herdr* servers, so every machine must run herdr,
255
+ including machines where you cannot install a multiplexer of your choosing. And
256
+ herdr detects state by matching terminal output, so adopting it means trading
257
+ push-based state from inside the agent for screen-scraping.
258
+
259
+ Taken from it: integration installs that write hooks into each agent's own
260
+ config directory (`murmur link pi`), reusing one authenticated connection, and
261
+ its own stated non-goals: no merged PTYs across machines, no moving work
262
+ between hosts, host as a lightweight label. Rejected: the always-present
263
+ sidebar, not for its ~4 columns but because it is fixed to one edge and cannot
264
+ become a horizontal strip, while a status row is overhead already paid.
265
+
266
+ ### T3 Code
267
+
268
+ An "agent harness control surface": a server owning agent sessions plus web,
269
+ desktop and mobile clients over one RPC WebSocket.
270
+
271
+ Its remote access is well ahead of herdr: direct ws/wss, bearer pairing, relay
272
+ tunnels, mesh-VPN serve and desktop-managed SSH, all shipped. But the aggregated view is unbuilt, by its own internals docs: multiple
273
+ live connections exist, a fused cross-machine agent overview does not.
274
+
275
+ It also does not support pi, and adding it is expensive: a provider needs a
276
+ driver plus an adapter implementing fourteen methods, and the reference
277
+ implementation is over 1700 lines. murmur's adapter problem is smaller for a
278
+ structural reason. T3 Code drives an agent it does not live inside, while
279
+ murmur's extension runs *in process* and calls the store directly.
280
+
281
+ Taken from it: the rule that *remoteness is expressed at the connection layer,
282
+ never by splitting the runtime* (murmur's channel seam is exactly this),
283
+ transport is not an identity, and environment identity as a stable UUID rather
284
+ than a hostname.
285
+
286
+ ### mu
287
+
288
+ An agent orchestrator: workstreams, a task DAG, agents in panes, isolated
289
+ workspaces. It already solved the hard half of the replication problem: machine
290
+ identity, per-peer watermarks, an op-log.
291
+
292
+ Two ideas taken directly. Sync is ambient rather than a daemon: every invocation
293
+ syncs before the verb, and no watcher outlives the command. And sync must never
294
+ fail a command (every ambient entry point is total; a dead peer warns and returns).
295
+
296
+ One idea deliberately not taken: mu's generic replicated KV. A generic op-log
297
+ needs conflict resolution, which single-writer partitioning skips entirely, and
298
+ a murmur with `put`/`del` over arbitrary entities would just *be* mu.
299
+
300
+ **Relationship:** murmur observes, mu orchestrates. Merging is plausible later,
301
+ since murmur would give mu global agent addressing and remote observation. But
302
+ remote *orchestration* is much harder than remote observation, and none of it is
303
+ murmur's problem.
304
+
305
+ ## Testing posture
306
+
307
+ Bug-driven, not coverage-driven. A thing earns a test when it can fail *without
308
+ you noticing*:
309
+
310
+ | Target | Why it can fail silently |
311
+ | ------ | ------------------------ |
312
+ | Fold precedence | A wrong glyph gets rationalized, not investigated |
313
+ | Staleness derivation | A dead peer keeps showing last-known state forever |
314
+ | Ingest idempotency | Duplicate rows after a partial read |
315
+ | Unknown-field preservation | Breaks against a future node you do not own |
316
+ | Retention keeping newest-per-agent | Idle agents vanish; reads as "not there" |
317
+
318
+ Not tested: ssh transport (OpenSSH's job), tmux wrappers (thin and loud), TUI
319
+ rendering, packaging.
320
+
321
+ New tests are verified by breaking the code they cover and watching them fail. A
322
+ test that has never failed has not been shown to test anything. During
323
+ development a test asserting a *wrong* behaviour was written twice, so this step
324
+ is not ceremony.
325
+
326
+ **The thing to know before adding a feature:** most bugs worth fixing here were
327
+ invisible to unit tests and to reading the code. A silently no-opping extension,
328
+ a remote jump broken by two independent shell-quoting layers, a 75-second hang
329
+ on an unreachable peer, ages that measured the wrong clock. All of them surfaced
330
+ by running the thing on two real machines. Unit tests protect the heuristics;
331
+ they do not tell you the tool is usable.
332
+
333
+ ## Deliberate non-goals
334
+
335
+ Each was considered and refused with reasons above:
336
+
337
+ - a daemon or listening socket
338
+ - interactive remote terminal rendering
339
+ - orchestration or work placement
340
+ - a generic put/del op-log
341
+ - HLC or clock reconciliation
342
+ - gossip replication (schema-compatible, not built)
343
+ - configuration knobs beyond peers and theme
344
+ - harnesses other than pi, multiplexers other than tmux, channels other than ssh
345
+
346
+ ## Known gaps
347
+
348
+ - **Nested tmux.** Jumping to a remote agent runs `ssh -t host tmux attach`
349
+ inside a local tmux window, which nests. Needs a distinct inner prefix or
350
+ `send-prefix`. Everyone in this space punts on it; herdr bans nesting outright.
351
+ - **Interactive attach is unverified.** Federation, staleness and the jump
352
+ target are verified across two machines over real ssh; sitting in a remote
353
+ pane and working in it is not.
354
+ - **The hardware-token path is verified only in the cold-fail direction.** The
355
+ second test node authenticates by key, so it never needed a warm socket.
356
+ - **Non-pi harnesses have no attention path.** Agents that cannot report from
357
+ inside themselves need an outside-in `notify` verb, which does not exist.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # murmur
2
+
3
+ **Every coding agent you have running, on every machine, in one list.**
4
+
5
+ You have agents on your laptop, your desktop, and a box somewhere else. One of
6
+ them is blocked waiting on you right now. Which one?
7
+
8
+ Today you find out by walking the machines. Attach here, glance there, try to
9
+ remember where you left that session. murmur answers the question in one
10
+ keystroke, then jumps you to the agent on whichever machine it turns out to be.
11
+
12
+ ```
13
+ state agent workstream host age / flags
14
+ ! blocked review the auth change api → devbox 4m
15
+ ▶ working Fix the picker filter murmur here
16
+ ✓ done migrate the fixtures api → devbox 12m
17
+ · idle worker-2 infra here crew
18
+ ```
19
+
20
+ Pick a row and press enter. Local agents are a window switch away; remote ones
21
+ open over ssh. The preview beside the list shows the last few lines the agent
22
+ printed, so you can tell "waiting on me" from "still thinking" without going
23
+ there at all.
24
+
25
+ ## Why it exists
26
+
27
+ The tools in this space are excellent at one machine and stop there.
28
+ [herdr](https://herdr.dev) is strictly one client to one server, and its
29
+ multi-client work is blocked behind an unfinished refactor. T3 Code has better
30
+ remote *access* than anything else here, but lists the aggregated view as
31
+ unbuilt in its own docs. Both infer agent state by matching terminal output.
32
+
33
+ murmur takes a different bet. The agent reports its own state from inside the
34
+ process, and the machines exchange nothing more complicated than "here is my
35
+ log since event N". Knowing what is happening is the hard part, and reporting
36
+ it from inside the agent is what makes it reliable.
37
+
38
+ ## What it is
39
+
40
+ - **A state layer over tmux:** tmux keeps owning your panes. murmur owns the
41
+ answer to "what is every agent doing right now".
42
+ - **Push-based state:** a pi extension reports from inside the agent. Nothing
43
+ screen-scrapes, and a crash is detected from a pid rather than guessed from
44
+ output.
45
+ - **No daemon, no listening socket, no master:** peers are pulled over ssh when
46
+ you run a command. Every node can aggregate; none is special.
47
+ - **Fast with one machine:** it replaced a local-only script and got quicker
48
+ doing it, 48 ms to first paint against 250 ms. Configuring zero peers is the
49
+ common case, and nothing about it is degraded.
50
+
51
+ ## What it is not
52
+
53
+ It does not orchestrate. It observes and connects, and never places work; that
54
+ is [`mu`](https://github.com/martintrojer/mu)'s job.
55
+
56
+ It is not a remote terminal. You can glance at a remote pane or jump to it, but
57
+ there is no frame streaming and no resize negotiation, which is most of why it
58
+ stays small.
59
+
60
+ It does not replace your multiplexer. See
61
+ [ARCHITECTURE.md](ARCHITECTURE.md#why-not-something-else) for the comparison
62
+ against herdr, T3 Code and `mu`.
63
+
64
+ ## Requirements
65
+
66
+ tmux, [pi](https://github.com/earendil-works/pi-coding-agent), `fzf`, and Node
67
+ 20+. For more than one machine: ssh access, and murmur installed on each.
68
+
69
+ ## Install
70
+
71
+ On every node that runs agents:
72
+
73
+ ```bash
74
+ npm install -g @martintrojer/murmur
75
+ murmur init # this node's identity
76
+ murmur link pi # install the agent-side extension
77
+ ```
78
+
79
+ `link pi` writes the extension into `~/.pi/agent/extensions/`, pinned to this
80
+ installation. Re-run it after upgrading murmur.
81
+
82
+ Then, on whichever machine you want to watch from:
83
+
84
+ ```bash
85
+ murmur peer add devbox # an ssh target; identity is discovered
86
+ murmur pick
87
+ ```
88
+
89
+ Bind it to a key and you have the whole interface:
90
+
91
+ ```tmux
92
+ bind -N "agent state picker" a display-popup -E -w 80% -h 60% "murmur pick"
93
+ ```
94
+
95
+ `murmur status` prints per-state counts for a status bar. Everything else is
96
+ `--help`.
97
+
98
+ ## Status
99
+
100
+ **0.1.0.** In daily use on one machine and verified across two over real ssh.
101
+ It is new and not battle-tested. The known gaps are listed at the end of
102
+ [ARCHITECTURE.md](ARCHITECTURE.md#known-gaps); the one most likely to annoy you
103
+ is that jumping to a remote agent nests tmux inside tmux, which every tool in
104
+ this space punts on.
105
+
106
+ The event schema is versioned on the wire and preserves fields it does not
107
+ recognise, so a newer node and an older one can already talk to each other.
108
+
109
+ ## Documentation
110
+
111
+ [ARCHITECTURE.md](ARCHITECTURE.md) explains how it works, the three ideas you
112
+ need before changing anything, why it exists rather than the alternatives, and
113
+ what is unfinished.
114
+
115
+ ---
116
+
117
+ *A murmuration: many independent agents, no leader, coherent from a distance.*