@martintrojer/murmur 0.2.1 → 0.2.2
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 +129 -81
- package/CHANGELOG.md +98 -0
- package/README.md +105 -70
- package/dist/cli.js +834 -269
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -2
- package/dist/index.js +19 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -354,7 +354,7 @@ where the system interpreter is least yours to touch.
|
|
|
354
354
|
"murmur_snapshot": 1,
|
|
355
355
|
"host_id": "1d2ee96e-3a94-41b2-90fa-5f1ee2f04276", // from identity.json
|
|
356
356
|
"display_name": "mtrojer-mac",
|
|
357
|
-
"murmur_version": "0.2.
|
|
357
|
+
"murmur_version": "0.2.2", // read from package.json, never restated
|
|
358
358
|
"generated_at": 1788105698997, // this node's clock at build time
|
|
359
359
|
"panes": [
|
|
360
360
|
{
|
|
@@ -611,6 +611,83 @@ rendering a picker needs targets, and only ones it can reach. Identity is
|
|
|
611
611
|
node's `host_id` and display name, which `peer add` records immediately rather
|
|
612
612
|
than throwing away.
|
|
613
613
|
|
|
614
|
+
Asymmetry is design with an unreported consequence: **if B does not peer A, B's
|
|
615
|
+
picker cannot see A's agents, and no local surface can say so.** From A
|
|
616
|
+
everything reads healthy, because from A it is.
|
|
617
|
+
|
|
618
|
+
`murmur doctor` surveys each peer over ssh and reports it. Two calls per peer —
|
|
619
|
+
`murmur export` for the `host_id`, `murmur peer list --json` for the roster —
|
|
620
|
+
because a snapshot structurally cannot carry a roster (below) and `peer list`
|
|
621
|
+
carries no `host_id`. Identity is compared on `host_id` only: names are local
|
|
622
|
+
handles and `hostname` can be a container id, so comparing by name would report
|
|
623
|
+
naming drift as asymmetry and miss genuine duplicates.
|
|
624
|
+
|
|
625
|
+
Asymmetry is *reported*, never called a fault, and exits 0. Flagging the normal
|
|
626
|
+
case — the laptop and the NAT'd server above — would train the operator to ignore
|
|
627
|
+
the command. Only a duplicate `host_id` and a snapshot-version mismatch are
|
|
628
|
+
problems, because both are murmur behaving provably wrongly. Skew is not
|
|
629
|
+
recomputed: `doctor` calls the same `versionCell` `peer list` uses.
|
|
630
|
+
|
|
631
|
+
"Island" is scoped to one hop and says so: *no peer that this node surveyed peers
|
|
632
|
+
this host*, not "no node in the fleet". `doctor` asks its own peers and stops —
|
|
633
|
+
multi-hop survey is a crawler, needing loop detection, a depth bound and a story
|
|
634
|
+
for a node reachable from B but not from here, all to report on machines this
|
|
635
|
+
node cannot fix anyway.
|
|
636
|
+
|
|
637
|
+
`doctor --topology` adds the fact hub advice needs and murmur otherwise lacks:
|
|
638
|
+
who can reach whom. One `ssh A "ssh B true"` per ordered pair — a bare `true`, so
|
|
639
|
+
it measures transport alone, since "unreachable" and "reachable but murmur
|
|
640
|
+
missing" have different fixes.
|
|
641
|
+
|
|
642
|
+
Which node *can* hub is then a set intersection over the matrix, not a
|
|
643
|
+
preference. A spoke counts only when reachability is proven in **both**
|
|
644
|
+
directions, since the spoke collects from the hub and the hub from the spoke.
|
|
645
|
+
When no node qualifies, nothing is recommended and the partition is reported.
|
|
646
|
+
|
|
647
|
+
Measured while building it: on the author's fleet two peers reach nothing, no
|
|
648
|
+
peer can resolve this node's own display name, and no whole-fleet hub exists. A
|
|
649
|
+
recommendation would have been wrong.
|
|
650
|
+
|
|
651
|
+
A failed probe is not a negative. It becomes `unreachable` only when the target
|
|
652
|
+
is demonstrably up — it answered this node's survey seconds earlier — and
|
|
653
|
+
otherwise it is `unknown`, because a firewall and a sleeping laptop are
|
|
654
|
+
indistinguishable from one dial and hub advice built on the confusion would flip
|
|
655
|
+
between runs as machines sleep. Nothing a survey learns is written to the store:
|
|
656
|
+
it is a diagnostic that reaches out, not state murmur caches, and caching it
|
|
657
|
+
would make `peer list` report facts no collect established.
|
|
658
|
+
|
|
659
|
+
**The snapshot carries no peer roster, deliberately.** Publishing one would be
|
|
660
|
+
the obvious way to make the fleet self-describing, and it is refused: a snapshot's
|
|
661
|
+
contract is "my panes, complete and authoritative", which is exactly what makes
|
|
662
|
+
"absent from a snapshot means absent" true. A roster is neither complete nor
|
|
663
|
+
authoritative — it is one node's local configuration, and its truth lives on the
|
|
664
|
+
node being described rather than the one describing. Putting membership into that
|
|
665
|
+
document would also make every membership question a format question, needing a
|
|
666
|
+
version bump and a coordinated fleet upgrade, which is a heavy price so soon
|
|
667
|
+
after 0.2.1 given that `parseSnapshot` rejects a mismatched version outright.
|
|
668
|
+
Surveying over ssh needs no format change and no fleet-wide upgrade, and it is
|
|
669
|
+
honest about what it is: a question asked now, not a fact murmur stores.
|
|
670
|
+
|
|
671
|
+
**Only `collect` could ever be relayed, which is why there is no broker.** Three
|
|
672
|
+
surfaces need a path to a pane's host, and they are not alike:
|
|
673
|
+
|
|
674
|
+
| surface | needs |
|
|
675
|
+
|---|---|
|
|
676
|
+
| collect | `ssh <target> murmur export` |
|
|
677
|
+
| glance (the picker's preview) | `ssh <target> tmux capture-pane` |
|
|
678
|
+
| jump | `ssh <target>`, interactively |
|
|
679
|
+
|
|
680
|
+
Glance and jump are inherently point-to-point: a captured pane and an interactive
|
|
681
|
+
session cannot be served by a third party holding neither. So a relay could only
|
|
682
|
+
deduplicate `collect` — and `collect` is the cheap one, ~400 bytes per pane and
|
|
683
|
+
~1.2KB per snapshot, measured.
|
|
684
|
+
|
|
685
|
+
A broker would therefore dedupe the cheapest of the three, leave both interactive
|
|
686
|
+
paths untouched, and add a daemon, a port and a second auth story to do it. That
|
|
687
|
+
is why gossip and a NATS-style bus lost: they address payload, and payload was
|
|
688
|
+
never the problem. The cost was one forked ssh per peer per repaint, fixed by
|
|
689
|
+
throttling — see the collect floor in limitation 3.
|
|
690
|
+
|
|
614
691
|
**Zero knobs.** Every exported setting is one the user can get wrong invisibly.
|
|
615
692
|
Two are irreducible: `peers` (only the operator knows their fleet) and `theme`.
|
|
616
693
|
Collection concurrency, deadlines and the staleness threshold are constants. The
|
|
@@ -639,83 +716,38 @@ deferral is the main reason murmur is small.
|
|
|
639
716
|
|
|
640
717
|
## Why not something else
|
|
641
718
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
horizontal strip, while a status row is overhead already paid.
|
|
675
|
-
|
|
676
|
-
### T3 Code
|
|
677
|
-
|
|
678
|
-
An "agent harness control surface": a server owning agent sessions plus web,
|
|
679
|
-
desktop and mobile clients over one RPC WebSocket.
|
|
680
|
-
|
|
681
|
-
Its remote access is well ahead of herdr: direct ws/wss, bearer pairing, relay
|
|
682
|
-
tunnels, mesh-VPN serve and desktop-managed SSH, all shipped. But the aggregated
|
|
683
|
-
view is unbuilt by its own internals docs — multiple live connections exist, a
|
|
684
|
-
fused cross-machine overview does not.
|
|
685
|
-
|
|
686
|
-
It also does not support pi, and adding it is expensive: a provider needs a
|
|
687
|
-
driver plus a fourteen-method adapter, and the reference implementation is over
|
|
688
|
-
1700 lines. murmur's adapter problem is smaller structurally: T3 Code drives an
|
|
689
|
-
agent it does not live inside, while murmur's extension runs *in process* and
|
|
690
|
-
calls the store directly.
|
|
691
|
-
|
|
692
|
-
Taken from it: the rule that *remoteness is expressed at the connection layer,
|
|
693
|
-
never by splitting the runtime* (murmur's channel seam is exactly this),
|
|
694
|
-
transport is not an identity, and environment identity as a stable UUID rather
|
|
695
|
-
than a hostname.
|
|
696
|
-
|
|
697
|
-
### mu
|
|
698
|
-
|
|
699
|
-
An agent orchestrator: workstreams, a task DAG, agents in panes, isolated
|
|
700
|
-
workspaces. It solved machine identity and cross-machine sync for its own
|
|
701
|
-
problem, which is a genuinely harder one: mu replicates *writes* from many
|
|
702
|
-
nodes, so it needs an op-log and per-peer watermarks.
|
|
703
|
-
|
|
704
|
-
Two ideas taken directly. Sync is ambient rather than a daemon: every invocation
|
|
705
|
-
syncs before the verb, and no watcher outlives the command. And sync never fails
|
|
706
|
-
a command — every ambient entry point is total, and a dead peer warns and
|
|
707
|
-
returns.
|
|
708
|
-
|
|
709
|
-
One idea deliberately not taken: mu's generic replicated KV. A generic op-log
|
|
710
|
-
needs conflict resolution, which single-writer-per-node caching skips entirely,
|
|
711
|
-
and a murmur with `put`/`del` over arbitrary entities would just *be* mu. The
|
|
712
|
-
same reasoning is why murmur ships no log of its own — nothing here is authored
|
|
713
|
-
by two parties, so there is nothing to merge.
|
|
714
|
-
|
|
715
|
-
**Relationship:** murmur observes, mu orchestrates. Merging is plausible later,
|
|
716
|
-
since murmur would give mu global agent addressing and remote observation. But
|
|
717
|
-
remote *orchestration* is much harder than remote observation, and none of it is
|
|
718
|
-
murmur's problem.
|
|
719
|
+
Version numbers and roadmap status are deliberately absent here: they were true
|
|
720
|
+
on the day they were checked and are the first thing to rot. What follows is the
|
|
721
|
+
structural difference, which does not move.
|
|
722
|
+
|
|
723
|
+
**Terminal multiplexers built for agents** (herdr and similar) own the panes and
|
|
724
|
+
infer state by matching terminal output. Two consequences: every machine must run
|
|
725
|
+
that multiplexer, which is not a choice you always have, and state is scraped
|
|
726
|
+
rather than reported. murmur takes tmux as given and has the agent report from
|
|
727
|
+
inside its own process.
|
|
728
|
+
|
|
729
|
+
**Harness control surfaces** (T3 Code and similar) run a server that owns agent
|
|
730
|
+
sessions, with clients over RPC. Their remote *access* is strong. The structural
|
|
731
|
+
cost is the adapter: driving an agent you do not live inside needs a per-harness
|
|
732
|
+
driver, where murmur's extension runs in-process and calls the store directly.
|
|
733
|
+
|
|
734
|
+
**Orchestrators** (`mu`) place work and replicate writes from many nodes, so they
|
|
735
|
+
need an op-log, watermarks and conflict resolution. murmur observes and never
|
|
736
|
+
places work, so single-writer-per-node caching removes all of it. **murmur
|
|
737
|
+
observes, mu orchestrates.** Merging is plausible — murmur would give mu global
|
|
738
|
+
agent addressing — but remote orchestration is a much harder problem than remote
|
|
739
|
+
observation, and it is not murmur's.
|
|
740
|
+
|
|
741
|
+
Taken from these tools, and worth stating because each is load-bearing above:
|
|
742
|
+
integration installs that write hooks into each agent's own config (`murmur link
|
|
743
|
+
pi`); reusing one authenticated connection; remoteness expressed at the
|
|
744
|
+
connection layer rather than by splitting the runtime; identity as a stable UUID
|
|
745
|
+
rather than a hostname; and ambient sync with no daemon, where every invocation
|
|
746
|
+
syncs before the verb and no watcher outlives the command.
|
|
747
|
+
|
|
748
|
+
Deliberately not taken: a generic replicated KV. A generic op-log needs conflict
|
|
749
|
+
resolution, which single-writer caching skips entirely, and a murmur with
|
|
750
|
+
`put`/`del` over arbitrary entities would just *be* mu.
|
|
719
751
|
|
|
720
752
|
## The extension's lifecycle assumptions
|
|
721
753
|
|
|
@@ -871,9 +903,25 @@ each was cheaper to accept than to solve:
|
|
|
871
903
|
only peer names and targets salvaged. A node serving the old event format is
|
|
872
904
|
reported as reachable-but-broken, which is the honest description: the two
|
|
873
905
|
cannot federate.
|
|
874
|
-
3. **No incremental sync.** Every collect transfers each peer's whole snapshot
|
|
875
|
-
|
|
876
|
-
|
|
906
|
+
3. **No incremental sync.** Every collect transfers each peer's whole snapshot,
|
|
907
|
+
so it is O(panes) rather than O(changes) — bounded by live pane count, and
|
|
908
|
+
small in absolute terms: ~400 bytes per pane and ~1.2KB for a whole snapshot,
|
|
909
|
+
measured. It is no longer *per tick*, though, and that was the part that
|
|
910
|
+
mattered. Fetch rate used to be tied to redraw rate, because `murmur status`
|
|
911
|
+
collects and tmux re-runs it every `status-interval`; the cost is quadratic in
|
|
912
|
+
a mesh and multiplied again per attached client, since the interval fires per
|
|
913
|
+
client. An ambient collect now skips a peer attempted within
|
|
914
|
+
`COLLECT_FLOOR_MS` ± `COLLECT_JITTER_MS / 2` (30s ± 10s), measured at 1.08s →
|
|
915
|
+
0.05s for `murmur status` against four real peers. Keyed on
|
|
916
|
+
`last_attempt_at`, not `fetched_at`: the expensive peer is the one that does
|
|
917
|
+
*not* answer, since it holds a forked ssh until `ConnectTimeout`, and keying
|
|
918
|
+
on success would exempt exactly the sleeping laptops the floor exists to stop
|
|
919
|
+
hammering. The jitter is drawn per peer per call and is what stops the floor
|
|
920
|
+
becoming a synchroniser — a bare floor makes a fleet converge on hitting one
|
|
921
|
+
machine in the same instant, permanently. Only ambient callers pass a floor:
|
|
922
|
+
`murmur collect` is a person asking, and the picker is a keypress, including
|
|
923
|
+
its `^r` refresh, where a floor would be a refresh key that silently does
|
|
924
|
+
nothing.
|
|
877
925
|
4. **No nested agents.** A second live process in one pane is refused and
|
|
878
926
|
reports nothing. A pane shows at most one agent.
|
|
879
927
|
5. **No remote liveness inference.** `owner_pid` never crosses the wire, so a
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,104 @@
|
|
|
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.2
|
|
7
|
+
|
|
8
|
+
**The picker's name column shows `murmur`, not `hacking/murmur`.** 0.2.1 fixed
|
|
9
|
+
*which* source a name comes from; this fixes how the session name is rendered
|
|
10
|
+
once it gets there. Session names are conventionally paths — that is how `tms`
|
|
11
|
+
and similar tools name them — and the last segment is the part that identifies
|
|
12
|
+
the work, so that is what the name column shows now.
|
|
13
|
+
|
|
14
|
+
That fallback is hit more often than it looks: an agent has no name from a richer
|
|
15
|
+
source unless mu set one or you ran pi's `/name`, because pi's own auto-namer runs
|
|
16
|
+
when a session closes. So a live hand-started pi — exactly the agent you are
|
|
17
|
+
looking at — fell through to the session name. The full path is not lost; it is
|
|
18
|
+
still in the stream column beside the name, so the row is strictly more
|
|
19
|
+
informative than before at the same width. Names you or mu chose are never
|
|
20
|
+
shortened.
|
|
21
|
+
|
|
22
|
+
**`murmur status` no longer reaches your peers on every status-bar repaint.**
|
|
23
|
+
Collection was driven by the tmux status bar — `status` collects, and tmux re-runs
|
|
24
|
+
it every `status-interval` — so fetch rate was tied to redraw rate. On a
|
|
25
|
+
four-peer node `murmur status` took 1.08s; it now takes 0.05s. If your status bar
|
|
26
|
+
felt sluggish, or you saw a lot of ssh processes on a machine doing nothing in
|
|
27
|
+
particular, that is this.
|
|
28
|
+
|
|
29
|
+
An ambient collect now skips a peer attempted in the last 30s (±10s of jitter, so
|
|
30
|
+
a fleet does not converge on hitting one machine at the same instant). Commands
|
|
31
|
+
you run yourself are unaffected: `murmur collect` fetches every peer, and so does
|
|
32
|
+
the picker, including its `^r` refresh. The visible trade is that a peer's data
|
|
33
|
+
can be up to ~40s old in the status bar rather than one tick old; the staleness
|
|
34
|
+
threshold is well above that, so a reachable peer still reads as fresh.
|
|
35
|
+
|
|
36
|
+
**New: `murmur doctor` reports what only a fleet-wide view can see.** Membership
|
|
37
|
+
is per node, so peering a machine does not mean it peers you — and if it does
|
|
38
|
+
not, its picker cannot see your agents. No local surface could tell you that,
|
|
39
|
+
because from your node everything reads healthy. `doctor` surveys each peer over
|
|
40
|
+
ssh and names it, along with duplicate hosts configured twice, snapshot-version
|
|
41
|
+
skew, one machine known under different names, and peers it could not survey at
|
|
42
|
+
all.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
$ murmur doctor
|
|
46
|
+
Surveyed 4 peers, 4 answered.
|
|
47
|
+
5 observations, nothing broken.
|
|
48
|
+
|
|
49
|
+
One-way peering
|
|
50
|
+
These do not peer this node, so their pickers cannot see its agents.
|
|
51
|
+
bubba does not peer mtrojer-mac
|
|
52
|
+
gardenpc does not peer mtrojer-mac
|
|
53
|
+
|
|
54
|
+
Do this
|
|
55
|
+
ssh bubba murmur peer add mtrojer-mac
|
|
56
|
+
ssh gardenpc murmur peer add mtrojer-mac
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It is read-only — nothing is written to the store and there is no `--fix`, since
|
|
60
|
+
every repair runs on another machine. Repairs are printed for you to run. Exit
|
|
61
|
+
status is 0 for observations and 1 only for a real problem, so it is safe in a
|
|
62
|
+
script, and asymmetry is an observation: reachability is *meant* to be
|
|
63
|
+
one-directional in places, and a check that failed on the normal case would be a
|
|
64
|
+
check you learn to ignore. `--json` gives the finding list with machine-readable
|
|
65
|
+
severity.
|
|
66
|
+
|
|
67
|
+
Worth knowing before you act on its advice: the suggested commands name your node
|
|
68
|
+
by the name it calls itself, and that name may not resolve from the peer's side.
|
|
69
|
+
On the author's fleet none of them could resolve it, so each suggestion needed an
|
|
70
|
+
address the far host can reach.
|
|
71
|
+
|
|
72
|
+
**New: `murmur doctor --topology` computes which fleet shapes are actually
|
|
73
|
+
possible.** Opt-in, because it costs one ssh dial per ordered pair (4 peers is
|
|
74
|
+
20) where the survey costs one per peer.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
$ murmur doctor --topology
|
|
78
|
+
Reachability 20 ordered pairs probed across 5 nodes
|
|
79
|
+
REACHES CANNOT REACH
|
|
80
|
+
mtrojer-mac all 4 -
|
|
81
|
+
bubba - mtrojer-mac gardenpc linuxpc macmini
|
|
82
|
+
linuxpc gardenpc macmini mtrojer-mac bubba
|
|
83
|
+
macmini bubba gardenpc linuxpc mtrojer-mac
|
|
84
|
+
|
|
85
|
+
Hub linuxpc serves {linuxpc, macmini}, leaves out mtrojer-mac, bubba, gardenpc
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
When no node can hub the fleet it recommends nothing and reports the partition,
|
|
89
|
+
which is the useful half — a hub half the fleet cannot reach is worse than no
|
|
90
|
+
hub. When one exists it prints the commands to build it, and states what a star
|
|
91
|
+
costs: spokes see the hub and the hub sees everyone, but spokes do not see each
|
|
92
|
+
other. A pair whose target was not demonstrably up is `unknown` rather than
|
|
93
|
+
unreachable, so a sleeping laptop is never reported as a firewall.
|
|
94
|
+
|
|
95
|
+
**The doctor report is tables, not prose.** Findings are grouped by kind with the
|
|
96
|
+
shared consequence stated once per group instead of once per row, and every
|
|
97
|
+
suggested command is collected under one deduplicated `Do this` block rather than
|
|
98
|
+
scattered through the text. The widest line went 129 columns to 78.
|
|
99
|
+
|
|
100
|
+
No upgrade coordination needed for any of this: the snapshot format is unchanged,
|
|
101
|
+
and `doctor` works against peers running 0.2.1. A peer too old for `peer list
|
|
102
|
+
--json` is reported as needing an upgrade rather than as broken.
|
|
103
|
+
|
|
6
104
|
## 0.2.1
|
|
7
105
|
|
|
8
106
|
**Agents are named by their tmux session, not by whatever process tmux found
|
package/README.md
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
**Every coding agent you have running, on every machine, in one list.**
|
|
4
4
|
|
|
5
|
-
You have agents on your laptop, your desktop, and a box somewhere else. One
|
|
6
|
-
|
|
5
|
+
You have agents on your laptop, your desktop, and a box somewhere else. One is
|
|
6
|
+
blocked waiting on you. Which one?
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
keystroke, then jumps you to the agent on whichever machine it turns out to be.
|
|
8
|
+
Without murmur you walk the machines to find out. murmur answers in one
|
|
9
|
+
keystroke and jumps you to the agent, wherever it is running.
|
|
11
10
|
|
|
12
11
|
```
|
|
13
12
|
state agent workstream host age / flags
|
|
@@ -17,53 +16,33 @@ keystroke, then jumps you to the agent on whichever machine it turns out to be.
|
|
|
17
16
|
· idle worker-2 infra here crew
|
|
18
17
|
```
|
|
19
18
|
|
|
20
|
-
Pick a row
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
35
|
-
everything I currently know". Knowing what is happening is the hard part, and reporting
|
|
36
|
-
it from inside the agent is what makes it reliable.
|
|
19
|
+
Pick a row, press enter. Local agents are a window switch; remote ones open over
|
|
20
|
+
ssh. The preview shows the last lines the agent printed, so you can tell "waiting
|
|
21
|
+
on me" from "still thinking" without going there.
|
|
37
22
|
|
|
38
23
|
## What it is
|
|
39
24
|
|
|
40
|
-
- **A state layer over tmux
|
|
41
|
-
|
|
42
|
-
- **Reported state, not scraped
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- **
|
|
50
|
-
|
|
51
|
-
- **Fast with one machine:** it replaced a local-only script and got quicker
|
|
52
|
-
doing it, ~50 ms to first paint against 250 ms. Configuring zero peers is the
|
|
53
|
-
common case, and nothing about it is degraded.
|
|
25
|
+
- **A state layer over tmux.** tmux owns your panes; murmur owns the answer to
|
|
26
|
+
"what is every agent doing right now".
|
|
27
|
+
- **Reported state, not scraped.** A pi extension reports from inside the agent,
|
|
28
|
+
so a crash is detected from a pid rather than guessed from output.
|
|
29
|
+
- **Current state only.** Each node publishes one complete snapshot. No history
|
|
30
|
+
and nothing to replay, which is why a peer's answer is replaced in one write
|
|
31
|
+
and absence means absence.
|
|
32
|
+
- **No daemon, no socket, no master.** Peers are pulled over ssh when you run a
|
|
33
|
+
command. Every node aggregates; none is special.
|
|
34
|
+
- **Fast with one machine.** ~50 ms to first paint. Zero peers is the common
|
|
35
|
+
case and nothing about it is degraded.
|
|
54
36
|
|
|
55
37
|
## What it is not
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
It does not replace your multiplexer. See
|
|
65
|
-
[ARCHITECTURE.md](ARCHITECTURE.md#why-not-something-else) for the comparison
|
|
66
|
-
against herdr, T3 Code and `mu`.
|
|
39
|
+
- **Not an orchestrator.** It observes and connects, never places work. That is
|
|
40
|
+
[`mu`](https://github.com/martintrojer/mu)'s job.
|
|
41
|
+
- **Not a remote terminal.** Glance at a pane or jump to it; there is no frame
|
|
42
|
+
streaming or resize negotiation. That deferral is most of why murmur is small.
|
|
43
|
+
- **Not a multiplexer.** tmux stays. See
|
|
44
|
+
[ARCHITECTURE.md](ARCHITECTURE.md#why-not-something-else) for how murmur
|
|
45
|
+
relates to adjacent tools.
|
|
67
46
|
|
|
68
47
|
## Requirements
|
|
69
48
|
|
|
@@ -180,8 +159,68 @@ picker to a key:
|
|
|
180
159
|
murmur peer list # your peers, and when each was last seen
|
|
181
160
|
murmur peer list --all # also ssh hosts that could become peers
|
|
182
161
|
murmur peer add devbox # an ssh target; identity is discovered
|
|
162
|
+
murmur doctor # survey each peer over ssh: what only a fleet view shows
|
|
163
|
+
murmur doctor --topology # also probe who can reach whom, and compute hub options
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`peer list` reads local state: what you configured, and when you last heard from
|
|
167
|
+
it. `doctor` dials out and asks each peer about *itself* — the only way to see
|
|
168
|
+
what no local surface can: **membership is per node, so peering a machine does
|
|
169
|
+
not mean it peers you.** If it does not, its picker cannot see your agents.
|
|
170
|
+
|
|
171
|
+
`doctor` writes nothing and repairs nothing; it prints the commands. Exit 0 for
|
|
172
|
+
observations, 1 only for a real problem, so it is safe in a script.
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
$ murmur doctor
|
|
176
|
+
Surveyed 4 peers, 4 answered.
|
|
177
|
+
5 observations, nothing broken.
|
|
178
|
+
|
|
179
|
+
Not visible to the fleet
|
|
180
|
+
mtrojer-mac none of the 4 surveyed peers can see this node
|
|
181
|
+
|
|
182
|
+
One-way peering
|
|
183
|
+
These do not peer this node, so their pickers cannot see its agents.
|
|
184
|
+
bubba does not peer mtrojer-mac
|
|
185
|
+
gardenpc does not peer mtrojer-mac
|
|
186
|
+
|
|
187
|
+
Do this
|
|
188
|
+
ssh bubba murmur peer add mtrojer-mac
|
|
189
|
+
ssh gardenpc murmur peer add mtrojer-mac
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Suggestions name this node as it calls itself, and that name may not resolve from
|
|
193
|
+
the peer's side — hence "check", not "run for you". `peer add` accepts a target
|
|
194
|
+
that does not answer yet and discovers identity on the first collect, so trying
|
|
195
|
+
costs nothing.
|
|
196
|
+
|
|
197
|
+
`--topology` is opt-in: it costs one dial per ordered pair (4 peers is 20) where
|
|
198
|
+
the survey costs one per peer. Plain `doctor` answers "is my fleet mutual?";
|
|
199
|
+
`--topology` answers "what shapes are possible here?", which you ask once while
|
|
200
|
+
setting up.
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
$ murmur doctor --topology
|
|
204
|
+
Reachability 20 ordered pairs probed across 5 nodes
|
|
205
|
+
REACHES CANNOT REACH
|
|
206
|
+
mtrojer-mac all 4 -
|
|
207
|
+
bubba - mtrojer-mac gardenpc linuxpc macmini
|
|
208
|
+
linuxpc gardenpc macmini mtrojer-mac bubba
|
|
209
|
+
macmini bubba gardenpc linuxpc mtrojer-mac
|
|
210
|
+
|
|
211
|
+
Hub linuxpc serves {linuxpc, macmini}, leaves out mtrojer-mac, bubba, gardenpc
|
|
183
212
|
```
|
|
184
213
|
|
|
214
|
+
Which node *can* hub is arithmetic on that matrix, not a preference. When none
|
|
215
|
+
qualifies, nothing is recommended and the partition is reported — a hub half the
|
|
216
|
+
fleet cannot reach is worse than no hub. A pair whose target was not
|
|
217
|
+
demonstrably up reads `unknown`, not unreachable: a sleeping laptop and a
|
|
218
|
+
firewall need different fixes.
|
|
219
|
+
|
|
220
|
+
A star also costs something the output states every time it names one: spokes see
|
|
221
|
+
the hub and the hub sees every spoke, but **spokes do not see each other**, since
|
|
222
|
+
`export` publishes local panes only.
|
|
223
|
+
|
|
185
224
|
Nodes being asleep or switched off is the normal state of a fleet, so nothing
|
|
186
225
|
warns about it on a polling path: `murmur status` and `murmur pick` stay silent
|
|
187
226
|
whatever the peers are doing. `murmur peer list` has a LAST SEEN column, and
|
|
@@ -192,35 +231,31 @@ could not reach.
|
|
|
192
231
|
bind -N "agent state picker" a display-popup -E -w 80% -h 60% "murmur pick"
|
|
193
232
|
```
|
|
194
233
|
|
|
195
|
-
In the picker: `^r` refreshes, `^p` cycles the preview,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
session, and its host, as literal substrings rather than scattered characters.
|
|
234
|
+
In the picker: `^r` refreshes, `^p` cycles the preview, `^u` clears the filter.
|
|
235
|
+
`M-b` / `M-w` / `M-d` / `M-x` filter to blocked, running, done or crashed; `M-a`
|
|
236
|
+
toggles orchestrated agents. Alt rather than ctrl because `^b` is tmux's prefix,
|
|
237
|
+
which a popup never receives. Typing matches agent name, workstream or tmux
|
|
238
|
+
session, and host, as literal substrings.
|
|
201
239
|
|
|
202
240
|
`murmur status` prints per-state counts for a status bar. Everything else is
|
|
203
241
|
`--help`.
|
|
204
242
|
|
|
205
243
|
## Jumping to a remote agent
|
|
206
244
|
|
|
207
|
-
A remote jump
|
|
208
|
-
|
|
209
|
-
|
|
245
|
+
A remote jump runs `ssh -t <host> tmux attach` in its own local tmux session,
|
|
246
|
+
named after the peer with a trailing `~`. That session sets two options on
|
|
247
|
+
itself, which is why it does not feel like nested tmux:
|
|
210
248
|
|
|
211
|
-
- `status off` —
|
|
212
|
-
|
|
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.
|
|
249
|
+
- `status off` — the remote's bar is the only one on screen.
|
|
250
|
+
- `prefix None` — `^b` goes straight to the remote. No `^b b` to learn.
|
|
215
251
|
|
|
216
|
-
Both are per-session, so your other sessions
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
Both are per-session, so your other sessions are unaffected. Leaving the remote
|
|
253
|
+
(inner `^b d`, the session ending, or the ssh dropping) returns you to the window
|
|
254
|
+
you jumped from and destroys the wrapper. Jumping to the same host twice reuses
|
|
255
|
+
one session.
|
|
220
256
|
|
|
221
|
-
The tradeoff:
|
|
222
|
-
|
|
223
|
-
remote, bind one key in the root table:
|
|
257
|
+
The tradeoff: inside the wrapper the local tmux has no prefix, so you cannot
|
|
258
|
+
reach it. For an escape hatch, bind one key in the root table:
|
|
224
259
|
|
|
225
260
|
```tmux
|
|
226
261
|
# Alt-Escape detaches out of a murmur wrapper session, and does nothing
|
|
@@ -235,7 +270,7 @@ already full-screen, and you land back at your shell prompt on exit.
|
|
|
235
270
|
|
|
236
271
|
## Status
|
|
237
272
|
|
|
238
|
-
**0.2.
|
|
273
|
+
**0.2.2.** In daily use on one machine and verified across two over real ssh.
|
|
239
274
|
It is new and not battle-tested. The known gaps and the accepted limitations are
|
|
240
275
|
listed at the end of [ARCHITECTURE.md](ARCHITECTURE.md#known-gaps).
|
|
241
276
|
|
|
@@ -246,9 +281,9 @@ shows each peer's version for exactly this. Upgrade the fleet together.
|
|
|
246
281
|
|
|
247
282
|
## Documentation
|
|
248
283
|
|
|
249
|
-
[ARCHITECTURE.md](ARCHITECTURE.md)
|
|
250
|
-
|
|
251
|
-
|
|
284
|
+
[ARCHITECTURE.md](ARCHITECTURE.md): the three independent facts the model rests
|
|
285
|
+
on, the design choices and what they cost, what murmur deliberately cannot do,
|
|
286
|
+
and what is unfinished.
|
|
252
287
|
|
|
253
288
|
---
|
|
254
289
|
|