@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/CHANGELOG.md CHANGED
@@ -3,6 +3,125 @@
3
3
  Notable changes per release. Written for someone deciding whether to upgrade,
4
4
  so it says what changed for a user rather than listing every commit.
5
5
 
6
+ ## 0.2.1
7
+
8
+ **Agents are named by their tmux session, not by whatever process tmux found
9
+ running in the window.** The picker's `agent` column showed `Python`, `node` and
10
+ `zsh` for real agents — pi's own interpreter, labelled "agent".
11
+
12
+ tmux's `automatic-rename` is on by default, which makes a window name simply the
13
+ foreground command, and murmur preferred that name over the session name. So any
14
+ agent without a mu agent name or a pi `/name` was listed under a process name,
15
+ and `hacking/murmur` — the string you actually search for — was hidden. A window
16
+ name is now recorded only when someone chose it, which means a window you renamed
17
+ yourself still wins, as does mu's own naming.
18
+
19
+ A row also no longer prints one string twice: the name and the stream column both
20
+ fell back to the session name, so an unnamed agent read `hacking/murmur
21
+ hacking/murmur` and spent thirteen columns saying nothing new.
22
+
23
+ Upgrading does not retroactively fix agents that are already running. The name is
24
+ recorded by the pane's own owner, so each agent's row is corrected when its pi
25
+ process next starts. A pi `/reload` is not enough — it re-runs the extension
26
+ inside the same process, which has the old code cached.
27
+
28
+ ## 0.2.0
29
+
30
+ **murmur now stores current state instead of an event log, and every node
31
+ publishes one complete snapshot of it.** This is a rewrite of the model, and it
32
+ is not compatible with 0.1.4 or anything before it. Upgrade every node together.
33
+
34
+ The old design appended events and folded them into a state per agent at read
35
+ time. It was the right shape for a tool that needed history, and murmur never
36
+ did: nothing replays events, the picker's preview is a live `capture-pane`, and
37
+ the only question anyone asks is "what is happening right now". The fold paid
38
+ for that flexibility in bugs, and they were not small ones.
39
+
40
+ What replaces it is three independent facts, each with exactly one writer:
41
+
42
+ - **activity** -- is a process working in this pane. Written only by that
43
+ process.
44
+ - **attention** -- does someone need to look at this pane: `done`, `blocked` or
45
+ `crashed`. Written by the owner, by an external notifier, or by local
46
+ reconciliation, and addressed by pane rather than by agent.
47
+ - **freshness** -- how recently we reached the node that reported. Known only by
48
+ the reader.
49
+
50
+ They are never collapsed into one value, and that is the fix for the worst bug
51
+ this project has had. `murmur notify` followed by a tmux focus hook used to
52
+ replace a running agent's state with `blocked` and then null its name, workstream
53
+ and driver -- on three live panes, with all three processes running. It happened
54
+ because everything was one enum in one table, so an attention writer could
55
+ overwrite an agent's state simply by writing the row it was allowed to write.
56
+ Attention now lives in its own table with no column an agent field could go in.
57
+ It is not that murmur checks; it is that there is nothing to check.
58
+
59
+ **A second agent in one pane is refused by the database, not by an environment
60
+ variable.** A pane holds at most one instrumented agent, enforced by a `UNIQUE`
61
+ constraint plus one liveness probe. A nested pi -- a subagent, or `pi` typed by
62
+ hand inside an agent's pane -- registers no handlers, writes nothing and paints
63
+ no badge. The previous mechanism passed a marker through the environment, which a
64
+ process launched in an unusual way could drop.
65
+
66
+ **Focus can no longer damage an agent.** `murmur clear` is one delete against the
67
+ attention table. The whitelist of clearable states, the "is this agent still
68
+ working" lookup and the metadata copy-forward are all gone, along with the
69
+ possibility of getting any of them wrong.
70
+
71
+ **A peer's whole state is replaced in one write, or not at all.** `murmur export`
72
+ takes no options and prints one JSON document describing every pane on that node.
73
+ A collect is one ssh round trip per peer, and the answer either replaces that
74
+ peer's cache entirely or leaves it untouched. Watermarks, epochs, `--since`, the
75
+ refetch-from-zero path and the wipe-detection machinery are all deleted -- none
76
+ of them are needed once a document is complete, because absence from it means
77
+ absence. A wiped node is no longer invisible to its peers for the same reason,
78
+ with nothing added to detect the wipe.
79
+
80
+ **A snapshot that does not validate is rejected before it is stored**, and the
81
+ peer is reported as reachable-but-broken with the reason on it, rather than
82
+ silently stale. That includes a version mismatch: fields are no longer carried
83
+ through unrecognised, because a reader that guesses about state a human acts on
84
+ is worse than one that says it cannot read the answer. `murmur peer list` shows
85
+ each peer's version so a bad pairing is visible before you debug it.
86
+
87
+ **A jump that fails changes nothing.** Pressing enter on a pane that has gone
88
+ away reports it and leaves every row alone. Only the node that owns a pane
89
+ retires it, on its next reconciliation. The picker's delete key is gone with the
90
+ per-agent replica rows it evicted, and its history preview is gone with the
91
+ history.
92
+
93
+ Breaking changes, in the order you will hit them:
94
+
95
+ - **`state.db` replaces `events.db`.** The old file is not migrated and is
96
+ deleted on first open. Peer names and targets survive; nothing else does.
97
+ - **`murmur export --since N` is gone.** The command takes no options.
98
+ - **`murmur status --json` has a new shape:** `{counts, orchestrated_counts,
99
+ panes, peers}`, where `counts` is keyed by the word a surface paints
100
+ (`crashed`, `blocked`, `done`, `running`, `idle`) and `panes` is a list of
101
+ panes rather than agents. `murmur status` without `--json` is unchanged.
102
+ - **Old and new nodes cannot federate.** An older peer serves the event format,
103
+ which this version rejects as an invalid document. It shows up as broken, with
104
+ a message saying so.
105
+ - **The SDK surface changed with the model.** The store's log methods, the fold
106
+ module and the wire envelope types are gone; `Store`, `Snapshot`, `PaneView`
107
+ and the pure `parseSnapshot` / `paneViews` / `renderState` replace them.
108
+
109
+ What this costs, stated so it is design rather than surprise: there is no history
110
+ of any kind, no incremental sync (each collect transfers a whole snapshot, which
111
+ is bounded by live pane count), no nested agents, and no inference about whether
112
+ a remote process is alive -- a remote pane's activity is whatever its own node
113
+ last said, and a stale node keeps its last-known values beside a warning.
114
+ ARCHITECTURE.md lists all eight accepted limitations.
115
+
116
+ Tests went 134 to 239 across 28 files, and they changed character with the model:
117
+ the ones that matter now assert what is *impossible* -- a notifier cannot touch
118
+ an agent row, a focus hook cannot change activity, no read path carries a pid --
119
+ several of them structurally, over the whole returned object graph rather than by
120
+ reading a type. Every new test was verified by breaking the code it covers. Test
121
+ processes are also now guaranteed not to touch the developer's own state, which
122
+ is not hypothetical: writing the contract for this rewrite corrupted the author's
123
+ live state three separate times, through the very bug being fixed.
124
+
6
125
  ## 0.1.4
7
126
 
8
127
  A review pass over the peer-collection code, and the bugs it found.
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.1.** 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