@martintrojer/murmur 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +740 -225
- package/CHANGELOG.md +97 -0
- package/README.md +136 -27
- package/dist/cli.js +1302 -833
- package/dist/cli.js.map +1 -1
- package/dist/extension/murmur-pi.js +189 -107
- 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 +479 -187
- package/dist/index.js +848 -607
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,103 @@
|
|
|
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.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
|
+
|
|
6
103
|
## 0.1.4
|
|
7
104
|
|
|
8
105
|
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
|
-
▶
|
|
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
|
|
35
|
-
|
|
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
|
-
- **
|
|
43
|
-
screen-scrapes, and a crash is detected from a pid rather than guessed
|
|
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,
|
|
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
|
|
86
|
-
installation
|
|
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.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 `
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
130
|
-
It is new and not battle-tested. The known gaps
|
|
131
|
-
[ARCHITECTURE.md](ARCHITECTURE.md#known-gaps)
|
|
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
|
-
|
|
136
|
-
|
|
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
|
|
141
|
-
|
|
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
|
|