@martintrojer/murmur 0.1.3 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,172 @@
1
+ # Changelog
2
+
3
+ Notable changes per release. Written for someone deciding whether to upgrade,
4
+ so it says what changed for a user rather than listing every commit.
5
+
6
+ ## 0.2.0
7
+
8
+ **murmur now stores current state instead of an event log, and every node
9
+ publishes one complete snapshot of it.** This is a rewrite of the model, and it
10
+ is not compatible with 0.1.4 or anything before it. Upgrade every node together.
11
+
12
+ The old design appended events and folded them into a state per agent at read
13
+ time. It was the right shape for a tool that needed history, and murmur never
14
+ did: nothing replays events, the picker's preview is a live `capture-pane`, and
15
+ the only question anyone asks is "what is happening right now". The fold paid
16
+ for that flexibility in bugs, and they were not small ones.
17
+
18
+ What replaces it is three independent facts, each with exactly one writer:
19
+
20
+ - **activity** -- is a process working in this pane. Written only by that
21
+ process.
22
+ - **attention** -- does someone need to look at this pane: `done`, `blocked` or
23
+ `crashed`. Written by the owner, by an external notifier, or by local
24
+ reconciliation, and addressed by pane rather than by agent.
25
+ - **freshness** -- how recently we reached the node that reported. Known only by
26
+ the reader.
27
+
28
+ They are never collapsed into one value, and that is the fix for the worst bug
29
+ this project has had. `murmur notify` followed by a tmux focus hook used to
30
+ replace a running agent's state with `blocked` and then null its name, workstream
31
+ and driver -- on three live panes, with all three processes running. It happened
32
+ because everything was one enum in one table, so an attention writer could
33
+ overwrite an agent's state simply by writing the row it was allowed to write.
34
+ Attention now lives in its own table with no column an agent field could go in.
35
+ It is not that murmur checks; it is that there is nothing to check.
36
+
37
+ **A second agent in one pane is refused by the database, not by an environment
38
+ variable.** A pane holds at most one instrumented agent, enforced by a `UNIQUE`
39
+ constraint plus one liveness probe. A nested pi -- a subagent, or `pi` typed by
40
+ hand inside an agent's pane -- registers no handlers, writes nothing and paints
41
+ no badge. The previous mechanism passed a marker through the environment, which a
42
+ process launched in an unusual way could drop.
43
+
44
+ **Focus can no longer damage an agent.** `murmur clear` is one delete against the
45
+ attention table. The whitelist of clearable states, the "is this agent still
46
+ working" lookup and the metadata copy-forward are all gone, along with the
47
+ possibility of getting any of them wrong.
48
+
49
+ **A peer's whole state is replaced in one write, or not at all.** `murmur export`
50
+ takes no options and prints one JSON document describing every pane on that node.
51
+ A collect is one ssh round trip per peer, and the answer either replaces that
52
+ peer's cache entirely or leaves it untouched. Watermarks, epochs, `--since`, the
53
+ refetch-from-zero path and the wipe-detection machinery are all deleted -- none
54
+ of them are needed once a document is complete, because absence from it means
55
+ absence. A wiped node is no longer invisible to its peers for the same reason,
56
+ with nothing added to detect the wipe.
57
+
58
+ **A snapshot that does not validate is rejected before it is stored**, and the
59
+ peer is reported as reachable-but-broken with the reason on it, rather than
60
+ silently stale. That includes a version mismatch: fields are no longer carried
61
+ through unrecognised, because a reader that guesses about state a human acts on
62
+ is worse than one that says it cannot read the answer. `murmur peer list` shows
63
+ each peer's version so a bad pairing is visible before you debug it.
64
+
65
+ **A jump that fails changes nothing.** Pressing enter on a pane that has gone
66
+ away reports it and leaves every row alone. Only the node that owns a pane
67
+ retires it, on its next reconciliation. The picker's delete key is gone with the
68
+ per-agent replica rows it evicted, and its history preview is gone with the
69
+ history.
70
+
71
+ Breaking changes, in the order you will hit them:
72
+
73
+ - **`state.db` replaces `events.db`.** The old file is not migrated and is
74
+ deleted on first open. Peer names and targets survive; nothing else does.
75
+ - **`murmur export --since N` is gone.** The command takes no options.
76
+ - **`murmur status --json` has a new shape:** `{counts, orchestrated_counts,
77
+ panes, peers}`, where `counts` is keyed by the word a surface paints
78
+ (`crashed`, `blocked`, `done`, `running`, `idle`) and `panes` is a list of
79
+ panes rather than agents. `murmur status` without `--json` is unchanged.
80
+ - **Old and new nodes cannot federate.** An older peer serves the event format,
81
+ which this version rejects as an invalid document. It shows up as broken, with
82
+ a message saying so.
83
+ - **The SDK surface changed with the model.** The store's log methods, the fold
84
+ module and the wire envelope types are gone; `Store`, `Snapshot`, `PaneView`
85
+ and the pure `parseSnapshot` / `paneViews` / `renderState` replace them.
86
+
87
+ What this costs, stated so it is design rather than surprise: there is no history
88
+ of any kind, no incremental sync (each collect transfers a whole snapshot, which
89
+ is bounded by live pane count), no nested agents, and no inference about whether
90
+ a remote process is alive -- a remote pane's activity is whatever its own node
91
+ last said, and a stale node keeps its last-known values beside a warning.
92
+ ARCHITECTURE.md lists all eight accepted limitations.
93
+
94
+ Tests went 134 to 239 across 28 files, and they changed character with the model:
95
+ the ones that matter now assert what is *impossible* -- a notifier cannot touch
96
+ an agent row, a focus hook cannot change activity, no read path carries a pid --
97
+ several of them structurally, over the whole returned object graph rather than by
98
+ reading a type. Every new test was verified by breaking the code it covers. Test
99
+ processes are also now guaranteed not to touch the developer's own state, which
100
+ is not hypothetical: writing the contract for this rewrite corrupted the author's
101
+ live state three separate times, through the very bug being fixed.
102
+
103
+ ## 0.1.4
104
+
105
+ A review pass over the peer-collection code, and the bugs it found.
106
+
107
+ Collection is driven by tmux re-running `murmur status` on a tick, and the peer
108
+ loop was serial: every peer paid the ssh timeout of every peer ahead of it, so
109
+ three sleeping laptops froze the status bar for thirty seconds. Peers are now
110
+ fetched concurrently, with a bound on the whole collect rather than only on each
111
+ peer.
112
+
113
+ Fixed, each with a symptom you could have hit:
114
+
115
+ - **A large peer could never sync.** The ssh export ran into Node's default
116
+ 1 MiB output limit, about 2,600 events. Past that the collect failed, and it
117
+ failed permanently: the watermark only advances on success, so every retry
118
+ re-requested the same oversized range. A reachable peer sat stale forever.
119
+ - **Jumping to an agent claimed success even when it failed.** A failed
120
+ new-window, ssh attach, or select-window all reported success, so the picker
121
+ closed and nothing moved, with no message. Jump now reports the failure.
122
+ - **A recovered host could stay marked "no tmux".** The recovery check counted
123
+ database inserts, which read zero on a retry after a partial write, so the
124
+ host stayed marked dead until it happened to author a new event.
125
+ - **The pi extension leaked a database handle per failed write**, inside a
126
+ process that can run for days.
127
+ - **ssh timeouts are sized to the status-bar tick** and deliberately
128
+ aggressive. A slow node is now rejected rather than allowed to hold up the
129
+ HUD; it shows stale until the next tick.
130
+ - **Retention ran only when peers were configured.** A single-machine node
131
+ never pruned, so its event log grew without bound.
132
+
133
+ Internal, no behaviour change: one shared ssh option list instead of three
134
+ hand-rolled copies, `clear`'s queries moved behind `Store`, and `STALENESS_MS`
135
+ states its value instead of deriving it from a collect interval nothing
136
+ enforced.
137
+
138
+ Tests went 83 to 103. Four existing tests could not fail and were rewritten;
139
+ every new test was verified by breaking the code it covers.
140
+
141
+ ## 0.1.3
142
+
143
+ Both fixes are about the peer columns being unreadable.
144
+
145
+ - `peer list` printed tab-separated fields with no header. Now a header and
146
+ aligned columns.
147
+ - `murmur pick` showed the node's self-reported hostname, which can be a
148
+ container id -- a string that appears nowhere else and cannot be typed at
149
+ `peer remove`. It now shows the peer name you configured.
150
+
151
+ ## 0.1.2
152
+
153
+ - **The picker had a doubled border inside a tmux popup.** `display-popup` draws
154
+ its own, so fzf's sat one character inside it. The popup is the normal way to
155
+ run the picker, so this was the common case.
156
+ - **Documented the focus-clear hooks**, which have to be wired by hand per node.
157
+ Without them a finished agent stays marked `done` forever and the picker fills
158
+ with rows that need nothing.
159
+
160
+ ## 0.1.1
161
+
162
+ - A stale badge is now reconciled, and a shell pane no longer clears the badge
163
+ of the agent pane next to it.
164
+ - Agents are searchable by tmux session, and typing matches substrings rather
165
+ than scattered characters.
166
+ - The delete key drops a stuck row from the picker.
167
+ - `--version` reads the manifest instead of a hardcoded string.
168
+ - Only `$TMUX_PANE` decides whether we are inside tmux.
169
+
170
+ ## 0.1.0
171
+
172
+ First release. Agent state across every machine you work on, in one view.
package/README.md CHANGED
@@ -12,7 +12,7 @@ keystroke, then jumps you to the agent on whichever machine it turns out to be.
12
12
  ```
13
13
  state agent workstream host age / flags
14
14
  ! blocked review the auth change api → devbox 4m
15
- working Fix the picker filter murmur here
15
+ running Fix the picker filter murmur here
16
16
  ✓ done migrate the fixtures api → devbox 12m
17
17
  · idle worker-2 infra here crew
18
18
  ```
@@ -31,21 +31,25 @@ remote *access* than anything else here, but lists the aggregated view as
31
31
  unbuilt in its own docs. Both infer agent state by matching terminal output.
32
32
 
33
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
34
+ process, and the machines exchange nothing more complicated than "here is
35
+ everything I currently know". Knowing what is happening is the hard part, and reporting
36
36
  it from inside the agent is what makes it reliable.
37
37
 
38
38
  ## What it is
39
39
 
40
40
  - **A state layer over tmux:** tmux keeps owning your panes. murmur owns the
41
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.
42
+ - **Reported state, not scraped:** a pi extension reports from inside the agent.
43
+ Nothing screen-scrapes, and a crash is detected from a pid rather than guessed
44
+ from output.
45
+ - **Current state only:** each node publishes one complete snapshot of what its
46
+ panes are doing right now. No history, no log, nothing to replay — which is
47
+ why a peer's whole answer can be replaced in one write and absence means
48
+ absence.
45
49
  - **No daemon, no listening socket, no master:** peers are pulled over ssh when
46
50
  you run a command. Every node can aggregate; none is special.
47
51
  - **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
52
+ doing it, ~50 ms to first paint against 250 ms. Configuring zero peers is the
49
53
  common case, and nothing about it is degraded.
50
54
 
51
55
  ## What it is not
@@ -82,8 +86,19 @@ murmur init # this node's identity
82
86
  murmur link pi # install the agent-side extension
83
87
  ```
84
88
 
85
- `link pi` writes the extension into `~/.pi/agent/extensions/`, pinned to this
86
- installation. Re-run it after upgrading murmur.
89
+ `link pi` writes a one-line extension into `~/.pi/agent/extensions/` that
90
+ re-exports this installation, so `npm install -g` is the whole upgrade and
91
+ there is nothing to re-run. Running agents keep the old code until they
92
+ restart, which is true of any extension change.
93
+
94
+ Re-run `link pi` only if the install path itself moves. `link pi --copy`
95
+ inlines the extension instead, which pins it to the version that wrote it and
96
+ does need re-linking after every upgrade — use it only if the extension has to
97
+ keep working when the murmur install is gone.
98
+
99
+ Order matters: without `murmur init` the extension loads and records nothing,
100
+ because a node with no identity has nothing to publish state as. `link pi` says
101
+ so if you skip it.
87
102
 
88
103
  Also on every node, in `.tmux.conf`, so a finished agent stops asking for
89
104
  attention once you look at it:
@@ -94,52 +109,146 @@ set-hook -g after-select-window "run-shell -b 'murmur clear --pane #{pane_id}
94
109
  set-hook -g client-session-changed "run-shell -b 'murmur clear --pane #{pane_id}'"
95
110
  ```
96
111
 
97
- These are per node and not optional. The `cleared` event they write replicates,
98
- so a node without them leaves its agents marked `done` in *every* peer's picker,
99
- not only its own status bar. Verify with `tmux show-hooks -g`: `set-hook`
100
- accepts a hook name your tmux does not have and exits 0, so a wrong name fails
101
- silently.
112
+ These are per node and not optional. `murmur clear` is the only thing that
113
+ acknowledges an attention request, and a node's own snapshot is what every peer
114
+ reads so a node without these hooks leaves its finished agents marked `done` in
115
+ *every* peer's picker, not only its own status bar. Verify with `tmux show-hooks
116
+ -g`: `set-hook` accepts a hook name your tmux does not have and exits 0, so a
117
+ wrong name fails silently.
118
+
119
+ Focus can only ever cancel a request for attention. It cannot stop a running
120
+ agent or alter anything the agent reported about itself, so there is no way to
121
+ wire these hooks such that looking at a pane damages the agent in it.
102
122
 
103
123
  The pane id is passed explicitly because hooks run in the tmux server, where
104
124
  `$TMUX_PANE` is unset, and because the badge belongs to the window while "you
105
125
  looked at it" is true of one pane. Without it, a window holding an agent and a
106
126
  shell clears when you focus the shell.
107
127
 
128
+ ### Harnesses other than pi
129
+
130
+ pi reports from inside itself, through the extension. codex and opencode have no
131
+ such hook -- they can only run a command when something happens -- so they use
132
+ `murmur notify`, which records an attention request for the pane it runs in:
133
+
134
+ ```toml
135
+ # ~/.codex/config.toml
136
+ notify = ["/bin/sh", "-lc", "murmur notify --source codex --event-type notify --title Codex"]
137
+ ```
138
+
139
+ The same four fields may arrive as a JSON object on stdin instead, which is
140
+ opencode's plugin form; flags win over the payload, so the line above behaves
141
+ identically either way.
142
+
143
+ **A hook is not an interactive shell, so check that `murmur` resolves in it.**
144
+ A notify hook inherits the PATH of whatever launched the harness, and `sh -l`
145
+ does not fix that -- `/bin/sh` is not your login shell and does not read your
146
+ zsh profile. A harness started from a terminal inherits a PATH with your npm
147
+ prefix on it and works; one started by a launcher, a daemon or a GUI may not,
148
+ and the failure is silent because a notify hook's output goes nowhere. Verify
149
+ from inside the harness, not from your terminal:
150
+
151
+ ```bash
152
+ murmur notify --source probe --message reachable && murmur status
153
+ # then undo it, or the pane stays badged:
154
+ murmur clear --pane "$TMUX_PANE"
155
+ ```
156
+
157
+ The probe is a real attention request: it records `blocked` and badges the
158
+ window, which is what makes it a genuine test of the path. `murmur clear` is
159
+ what takes it back, and focusing the pane does the same if you have the hooks
160
+ above installed.
161
+
162
+ If `murmur` is not reachable there, give the hook the absolute path
163
+ (`command -v murmur` from your shell) rather than relying on PATH.
164
+
165
+ `notify` is the one path where a process that does not own a pane may write
166
+ about it, and it is narrow by construction rather than by convention: an
167
+ attention request has no field for an agent id, a pid, an activity or any owner
168
+ metadata, so it cannot make a claim about a process even by mistake. It says
169
+ `blocked` and nothing else; running, done and crashed stay the pane owner's and
170
+ murmur's own reconciliation's. Outside tmux it records nothing and exits 0, so it
171
+ cannot break the caller's own exit code.
172
+
173
+ A pane reached only this way — a codex agent murmur never instrumented — is a
174
+ full row in the list: it shows up, it is filterable, and enter jumps to it.
175
+
108
176
  Then, on whichever machine you want to watch from, add the peers and bind the
109
177
  picker to a key:
110
178
 
111
179
  ```bash
180
+ murmur peer list # your peers, and when each was last seen
181
+ murmur peer list --all # also ssh hosts that could become peers
112
182
  murmur peer add devbox # an ssh target; identity is discovered
113
183
  ```
114
184
 
185
+ Nodes being asleep or switched off is the normal state of a fleet, so nothing
186
+ warns about it on a polling path: `murmur status` and `murmur pick` stay silent
187
+ whatever the peers are doing. `murmur peer list` has a LAST SEEN column, and
188
+ `murmur collect` -- which you run deliberately -- prints one line per peer it
189
+ could not reach.
190
+
115
191
  ```tmux
116
192
  bind -N "agent state picker" a display-popup -E -w 80% -h 60% "murmur pick"
117
193
  ```
118
194
 
119
- In the picker: `^r` refreshes, `^p` cycles the preview, and `del` drops a stuck
120
- row. `^b` / `^w` / `^d` / `^x` filter to blocked, working, done or crashed, and
121
- `^a` clears the filter. Typing matches the agent name, its workstream or tmux
195
+ In the picker: `^r` refreshes, `^p` cycles the preview, and `^u` clears the
196
+ filter. `M-b` / `M-w` / `M-d` / `M-x` filter to blocked,
197
+ running, done or crashed, and `M-a` toggles orchestrated agents in and out of
198
+ the list. Alt rather than ctrl because `^b` is tmux's own prefix, which a popup
199
+ never receives. Typing matches the agent name, its workstream or tmux
122
200
  session, and its host, as literal substrings rather than scattered characters.
123
201
 
124
202
  `murmur status` prints per-state counts for a status bar. Everything else is
125
203
  `--help`.
126
204
 
205
+ ## Jumping to a remote agent
206
+
207
+ A remote jump opens the `ssh -t <host> tmux attach` in a local tmux session of
208
+ its own, named after the peer with a trailing `~`. That session sets two options
209
+ on itself, and both are why the jump does not feel like nested tmux:
210
+
211
+ - `status off` — no local status bar, so the remote's own bar is the only one on
212
+ screen and the jump reads as a full-screen ssh.
213
+ - `prefix None` — no local prefix at all, so `^b` goes straight to the remote.
214
+ No `^b b`, and no second prefix to learn.
215
+
216
+ Both are per-session, so your other sessions keep their prefix and status bar.
217
+ When you leave the remote — inner `^b d`, the remote session ending, or the ssh
218
+ dropping — the wrapper returns you to the exact window you jumped from and
219
+ disappears. Jumping to the same host twice reuses the one session.
220
+
221
+ The tradeoff: while you are inside the wrapper, the local tmux has no prefix, so
222
+ you cannot reach it. If you want an escape hatch that does not involve the
223
+ remote, bind one key in the root table:
224
+
225
+ ```tmux
226
+ # Alt-Escape detaches out of a murmur wrapper session, and does nothing
227
+ # elsewhere. Deliberately not M-b or another Alt letter: a root-table binding
228
+ # is consumed before any pane, so it would eat the picker's own M-b / M-w /
229
+ # M-d / M-x filters -- the same class of collision that made ^b useless there.
230
+ bind -n M-Escape if-shell -F '#{m:*~,#{session_name}}' detach-client
231
+ ```
232
+
233
+ Outside tmux none of this applies: `murmur pick` runs the ssh directly, which is
234
+ already full-screen, and you land back at your shell prompt on exit.
235
+
127
236
  ## Status
128
237
 
129
- **0.1.3.** In daily use on one machine and verified across two over real ssh.
130
- It is new and not battle-tested. The known gaps are listed at the end of
131
- [ARCHITECTURE.md](ARCHITECTURE.md#known-gaps); the one most likely to annoy you
132
- is that jumping to a remote agent nests tmux inside tmux, which every tool in
133
- this space punts on.
238
+ **0.2.0.** In daily use on one machine and verified across two over real ssh.
239
+ It is new and not battle-tested. The known gaps and the accepted limitations are
240
+ listed at the end of [ARCHITECTURE.md](ARCHITECTURE.md#known-gaps).
134
241
 
135
- The event schema is versioned on the wire and preserves fields it does not
136
- recognise, so a newer node and an older one can already talk to each other.
242
+ **All nodes must run the same murmur version.** The snapshot format is versioned
243
+ and a mismatch is rejected rather than guessed at, so a node running older code
244
+ is reported as reachable-but-broken with the reason on it — `murmur peer list`
245
+ shows each peer's version for exactly this. Upgrade the fleet together.
137
246
 
138
247
  ## Documentation
139
248
 
140
- [ARCHITECTURE.md](ARCHITECTURE.md) explains how it works, the three ideas you
141
- need before changing anything, why it exists rather than the alternatives, and
142
- what is unfinished.
249
+ [ARCHITECTURE.md](ARCHITECTURE.md) explains how it works: the three independent
250
+ facts the whole model rests on, why it exists rather than the alternatives, what
251
+ it deliberately cannot do, and what is unfinished.
143
252
 
144
253
  ---
145
254