@mu-crew/mu 3.1.0 → 3.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/AGENTS.md +203 -588
- package/README.md +80 -308
- package/dist/cli.js +6106 -4716
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +786 -0
- package/dist/extension/mu-pi.js.map +1 -0
- package/dist/index.d.ts +738 -536
- package/dist/index.js +6617 -5785
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +178 -1008
- package/docs/ROADMAP.md +88 -169
- package/docs/VISION.md +206 -407
- package/docs/VOCABULARY.md +202 -455
- package/docs/architecture/control-socket.md +119 -0
- package/docs/architecture/dag.md +113 -0
- package/docs/architecture/mux.md +163 -0
- package/docs/architecture/ops-log.md +140 -0
- package/docs/architecture/sdk.md +185 -0
- package/docs/architecture/sync.md +129 -0
- package/docs/architecture/tui.md +146 -0
- package/docs/guide/README.md +25 -0
- package/docs/guide/backends.md +61 -0
- package/docs/guide/cleanup.md +74 -0
- package/docs/guide/delegate.md +87 -0
- package/docs/guide/dispatch.md +137 -0
- package/docs/guide/getting-started.md +177 -0
- package/docs/guide/recovery.md +133 -0
- package/docs/guide/remote.md +74 -0
- package/docs/guide/sql.md +76 -0
- package/docs/guide/stop-a-worker.md +64 -0
- package/docs/guide/sync.md +78 -0
- package/docs/guide/tui.md +98 -0
- package/docs/guide/upgrade.md +76 -0
- package/docs/history/CHANGELOG-pre-3.md +4866 -0
- package/docs/reference/env.md +87 -0
- package/docs/reference/naming.md +82 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
- package/docs/specs/2026-09-29-task-substates.md +1 -1
- package/package.json +4 -4
- package/skills/mu/REMOTE_WORKERS.md +186 -491
- package/skills/mu/SKILL.md +142 -217
- package/docs/HANDOVER.md +0 -486
- package/docs/USAGE_GUIDE.md +0 -2637
|
@@ -1,178 +1,60 @@
|
|
|
1
1
|
# Remote workers
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
none.
|
|
3
|
+
mu has no remote backend, ssh code, or host registry. This is a recipe
|
|
4
|
+
plus the traps that cost time.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
the
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
attach pane open — it holds the only ssh channel and silently breaks
|
|
12
|
-
`git fetch`, `murmur collect` and every other ssh. Attach to look, then
|
|
13
|
-
`mu agent close`. See § Never leave an attach pane open. This guidance
|
|
14
|
-
concerns interactive attachment, not mu's automatic agent-state reads.
|
|
15
|
-
|
|
16
|
-
---
|
|
6
|
+
**If you read one thing:** on a session-capped host, an open attach pane
|
|
7
|
+
holds the only ssh channel and silently breaks `git fetch`,
|
|
8
|
+
`murmur collect`, and every other ssh. Attach to look, then
|
|
9
|
+
`mu agent close`.
|
|
17
10
|
|
|
18
11
|
## The model
|
|
19
12
|
|
|
20
|
-
**The pane is local, the process is remote.**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
**murmur sees the REMOTE pane** — the opposite of the line above. Its
|
|
33
|
-
extension runs inside the agent's process, so it claims the pane on the
|
|
34
|
-
HOST; mu's local pane holds an ssh client and reports nothing. Two
|
|
35
|
-
addresses, one worker.
|
|
36
|
-
|
|
37
|
-
murmur ties them back together by looking for the AGENT NAME in your
|
|
38
|
-
local pane's command line. When it matches, the remote row shows
|
|
39
|
-
`attached here %N` and enter focuses your existing pane instead of
|
|
40
|
-
opening a second connection — which matters on a capped host, where the
|
|
41
|
-
second one fails.
|
|
42
|
-
|
|
43
|
-
Both recipes below work, for the same reason: the agent name is in the
|
|
44
|
-
argv either way. A direct spawn carries it in the remote command; the
|
|
45
|
-
detached shape carries it in the session name, because you named the
|
|
46
|
-
session after the agent. **Keep doing that** — `-s mu-<agent>` is what
|
|
47
|
-
makes the attachment findable, and a session named anything else costs
|
|
48
|
-
you the hint.
|
|
49
|
-
|
|
50
|
-
Do not bother putting `MU_AGENT_NAME=` in front of the attach command
|
|
51
|
-
to help it along. An environment prefix is consumed by your shell and
|
|
52
|
-
never reaches the process arguments, which is all `ps` reports on macOS.
|
|
53
|
-
|
|
54
|
-
What mu does NOT know about a remote agent: the workspace. There is no
|
|
55
|
-
`vcs_workspaces` row, so no `mu workspace list / refresh / commits /
|
|
56
|
-
free`, no `behind` column, no staleness warning on claim, and no
|
|
57
|
-
auto-free on close. You own that bookkeeping.
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Never leave an attach pane open
|
|
62
|
-
|
|
63
|
-
On a host that caps sessions per connection, **your attach pane holds
|
|
64
|
-
the only ssh channel**. While it is open, `git fetch`, `git push`,
|
|
65
|
-
`murmur collect`, `rsync`, and plain ssh can fail with the misleading
|
|
66
|
-
`Permission denied (keyboard-interactive)` error.
|
|
67
|
-
|
|
68
|
-
**Attach to look, then close immediately.** `mu agent close <name>`
|
|
69
|
-
detaches without stopping a detached-tmux agent. mu reads agent state
|
|
70
|
-
through murmur automatically; use mule for orchestrator commands because
|
|
71
|
-
its separate ControlPath cannot contend with the attach pane. See
|
|
72
|
-
§ When the host limits concurrent sessions for diagnosis and the
|
|
73
|
-
remote-agent setup.
|
|
74
|
-
|
|
75
|
-
When a collect fails, current murmur names an attachment that holds the
|
|
76
|
-
channel. Otherwise inspect it directly:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
ps -o pid=,command= -ax | grep "[s]sh <host>"
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Better: attach with the peer's jump command
|
|
83
|
-
|
|
84
|
-
If murmur knows the host, ask it how to reach the host interactively
|
|
85
|
-
rather than hardcoding ssh:
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
JUMP=$(murmur peer list --json | jq -r '.[]|select(.name=="dev").jump_command')
|
|
89
|
-
mu agent spawn worker-1 -w big --command "${JUMP//\{pane\}/mu-worker-1}"
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
That command may name a transport taking **no** ssh session at all,
|
|
93
|
-
which removes the contention rather than managing it.
|
|
94
|
-
|
|
95
|
-
**A spawn that lands on an auth prompt looks exactly like a healthy
|
|
96
|
-
one.** Measured: with the slot busy, `ssh dev -t "tmux attach"` fell
|
|
97
|
-
back to a fresh connection and stopped at `Enter a passcode:`. The spawn
|
|
98
|
-
succeeded, the row appeared, `mu agent list` showed `needs_input` — and
|
|
99
|
-
`mu agent send` pasted the whole prompt into the passcode field and
|
|
100
|
-
reported success. The agent received nothing.
|
|
101
|
-
|
|
102
|
-
So confirm a remote agent actually got the work before trusting it
|
|
103
|
-
(`mu agent read <name> -n 20`, or its context percentage), and never
|
|
104
|
-
send anything sensitive to an unconfirmed pane.
|
|
105
|
-
|
|
106
|
-
---
|
|
13
|
+
- **The pane is local, the process is remote.** The pane's foreground
|
|
14
|
+
process is ssh. `send`, `read`, and the reaper work unchanged. The pi
|
|
15
|
+
control socket rides the same ssh as a `-L` forward, so pi state,
|
|
16
|
+
`wait`, and `abort` are exact.
|
|
17
|
+
- **One orchestrator DB.** Never run a second mu on the host.
|
|
18
|
+
`tasks.owner_id` is an FK into the machine-local `agents` table, so a
|
|
19
|
+
remote mu cannot claim tasks in your DAG.
|
|
20
|
+
- **mu does not know the remote workspace**: no `mu workspace` verbs,
|
|
21
|
+
staleness warning, or auto-free. You own it.
|
|
22
|
+
- **Everything is orchestrator-pull.** The host usually has no route
|
|
23
|
+
back to your laptop. Push setup out, fetch commits back.
|
|
107
24
|
|
|
108
25
|
## The recipe
|
|
109
26
|
|
|
110
27
|
```bash
|
|
111
|
-
# 1. WORKSPACE
|
|
28
|
+
# 1. WORKSPACE: you create it; --workspace is local-only
|
|
112
29
|
ssh dev 'git -C ~/repo worktree add ~/ws/worker-1'
|
|
113
30
|
|
|
114
|
-
# 2. RECORD
|
|
31
|
+
# 2. RECORD: location and per-worker baseline, in one note
|
|
115
32
|
mu task note t1 -w big "REMOTE: dev:~/ws/worker-1
|
|
116
33
|
REMOTE_BASE: worker-1:$(ssh dev 'cd ~/ws/worker-1 && git rev-parse HEAD')"
|
|
117
34
|
|
|
118
|
-
# 3. SPAWN
|
|
35
|
+
# 3. SPAWN: env goes INSIDE the command (tmux -e stops at the hop).
|
|
36
|
+
# remote-env prints the socket forward and the identity env; it runs nothing.
|
|
37
|
+
# $SHELL -ilc: ssh's non-interactive shell skips ~/.zshrc (PATH, provider env).
|
|
38
|
+
eval "$(mu agent remote-env worker-1 -w big --shell)" # MU_SSH_ARGS, MU_REMOTE_ENV
|
|
119
39
|
mu agent spawn worker-1 -w big --command \
|
|
120
|
-
|
|
40
|
+
"ssh $MU_SSH_ARGS dev -t 'cd ~/ws/worker-1 && $MU_REMOTE_ENV \$SHELL -ilc \"pi --approve\"'"
|
|
121
41
|
|
|
122
|
-
# 4. CLAIM + SEND
|
|
42
|
+
# 4. CLAIM + SEND: as for a local agent
|
|
123
43
|
mu task claim t1 -w big --for worker-1 --evidence 'remote on dev'
|
|
124
|
-
mu agent send worker-1 -w big '...'
|
|
125
|
-
|
|
126
|
-
# 5. WAIT — run the claim's one-shot Next: command once per turn
|
|
127
|
-
# Set a deadline; at expiry read the pane, then release or re-dispatch.
|
|
128
|
-
job=$(mule run --host dev --max-secs 30 \
|
|
129
|
-
'cd ~/ws/worker-1 && git rev-parse HEAD 2>/dev/null || echo unreadable')
|
|
130
|
-
mule wait "$job" >/dev/null && sha=$(mule tail "$job")
|
|
131
|
-
case "$sha" in (*[!0-9a-fA-F]*|'') :;; (*) [ "${#sha}" -eq 40 ] && \
|
|
132
|
-
{ [ "$sha" = 24481fe24481fe24481fe24481fe24481fe2448 ] || \
|
|
133
|
-
mu task close t1 -w big --evidence "worker-1 committed $sha"; };; esac
|
|
134
|
-
|
|
135
|
-
# 6. COLLECT — fetch straight from the remote worktree
|
|
136
|
-
git fetch "ssh://dev/~/ws/worker-1" HEAD && git cherry-pick FETCH_HEAD
|
|
137
|
-
```
|
|
44
|
+
mu agent send worker-1 -w big --fresh '...'
|
|
138
45
|
|
|
139
|
-
|
|
140
|
-
`mu state` and `mu task wait` do not care where a pane's process runs
|
|
141
|
-
— task status is a row in YOUR database, written by you, so a wait on
|
|
142
|
-
it is exact.
|
|
46
|
+
# 5. WAIT: run the claim's one-shot Next: command once per turn
|
|
143
47
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
by murmur's collect floor (30 seconds ± 10 seconds) plus that cache;
|
|
148
|
-
their `since` value is the row's `updated_at`, so idle and stall age is
|
|
149
|
-
approximate.
|
|
150
|
-
|
|
151
|
-
| waiting on | remote? |
|
|
152
|
-
| --- | --- |
|
|
153
|
-
| `mu task wait` (task status) | exact — a DB poll, once something closes the task |
|
|
154
|
-
| the reaper | fires, but see below |
|
|
155
|
-
| stall detection | exact state from murmur; approximate age |
|
|
156
|
-
| `mu agent wait --first` | exact state from murmur |
|
|
157
|
-
|
|
158
|
-
Without murmur, remote agent state is `unknown`. `mu agent wait` does not
|
|
159
|
-
fire on `unknown`, and `unknown` never counts as a task stall. Run
|
|
160
|
-
`mu doctor` for the missing-source reason.
|
|
161
|
-
|
|
162
|
-
The reaper is right for a direct spawn — the connection dying really
|
|
163
|
-
does kill that agent — and wrong for a detached-tmux one, where the
|
|
164
|
-
agent outlives the ssh but mu reaps the task anyway. See § A dropped
|
|
165
|
-
connection reaps the task but NOT the commit.
|
|
166
|
-
|
|
167
|
-
The division is: mu owns the work; murmur reports what the agent is doing.
|
|
48
|
+
# 6. COLLECT: fetch straight from the remote worktree
|
|
49
|
+
git fetch "ssh://dev/~/ws/worker-1" HEAD && git cherry-pick FETCH_HEAD
|
|
50
|
+
```
|
|
168
51
|
|
|
169
|
-
###
|
|
52
|
+
### Step 2: the note is load-bearing
|
|
170
53
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
baseline per worker and use one bounded mule job for every path:
|
|
54
|
+
Keep the exact forms `REMOTE: <host>:<path>` and
|
|
55
|
+
`REMOTE_BASE: <agent>:<sha>`. `mu task claim --for` builds the
|
|
56
|
+
poll-and-close command from them, and `mu state` lists `REMOTE:` lines.
|
|
57
|
+
For a crew, poll every path in one bounded mule job:
|
|
176
58
|
|
|
177
59
|
```bash
|
|
178
60
|
mule run --max-secs 30 'for d in ~/ws/*/; do printf "%s %s\\n" \
|
|
@@ -180,418 +62,231 @@ mule run --max-secs 30 'for d in ~/ws/*/; do printf "%s %s\\n" \
|
|
|
180
62
|
mule wait <job>; mule tail <job>
|
|
181
63
|
```
|
|
182
64
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
### On step 5 — wait on commits
|
|
187
|
-
|
|
188
|
-
**Poll once per turn, never sleep in a tool call.** A `while true` loop
|
|
189
|
-
with `sleep` holds the call; aborting it can leave remote work and a
|
|
190
|
-
capped ssh channel running. Set a wall-clock deadline before dispatch.
|
|
191
|
-
At expiry, read the pane, then release or re-dispatch instead of extending
|
|
192
|
-
the wait silently.
|
|
65
|
+
Accept only 40 hex characters. `unreadable` or an empty field means
|
|
66
|
+
retry next turn, not progress.
|
|
193
67
|
|
|
194
|
-
|
|
195
|
-
between other work. Bare ssh polls can be refused with an empty sha,
|
|
196
|
-
which looks like progress. The command captures the new sha and closes
|
|
197
|
-
the task with that sha as evidence. The commit tells you;
|
|
198
|
-
the DAG stays IN_PROGRESS until the orchestrator closes it. This feeds
|
|
199
|
-
`mu task wait` so blocked tasks advance; it does not replace it.
|
|
68
|
+
### Step 3: the control-socket forward
|
|
200
69
|
|
|
201
|
-
|
|
70
|
+
Spawn reports `ctl ok` when all of these hold (`mu doctor` has a ctl
|
|
71
|
+
row per agent):
|
|
202
72
|
|
|
203
|
-
|
|
204
|
-
|
|
73
|
+
- The host has the mu extension: run `mu link pi` there.
|
|
74
|
+
- The ssh is direct. `$MU_SSH_ARGS` sets `ControlMaster=no
|
|
75
|
+
ControlPath=none`; a multiplexed ssh never binds the forward and spawn
|
|
76
|
+
reports `ctl missing`.
|
|
77
|
+
- sshd allows the forward. A refused forward kills the pane at spawn
|
|
78
|
+
(`ExitOnForwardFailure=yes`). Check `AllowStreamLocalForwarding`.
|
|
205
79
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
### On step 6 — fetching from a worktree
|
|
80
|
+
A dead ssh leaves the socket file, which probes `refused`, never a
|
|
81
|
+
stale `ok`. Non-pi agents get state from murmur, lagging 30 s ± 10 s
|
|
82
|
+
plus a 10 s cache.
|
|
211
83
|
|
|
212
|
-
|
|
213
|
-
**directly**. No shared remote, no push, no bare repo in between. This
|
|
214
|
-
is the part people expect to be hard and it is not.
|
|
84
|
+
### Step 5: poll once per turn
|
|
215
85
|
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
-
|
|
221
|
-
|
|
86
|
+
- Never `sleep` in a tool call. Aborting the loop can leave remote work
|
|
87
|
+
and a capped channel running.
|
|
88
|
+
- Set a deadline before dispatch. At expiry, read the pane, then release
|
|
89
|
+
or re-dispatch.
|
|
90
|
+
- On a capped host, run `Next:` through mule. A refused bare ssh returns
|
|
91
|
+
an empty sha that looks like progress.
|
|
222
92
|
|
|
223
|
-
###
|
|
93
|
+
### Step 6: fetch from the worktree
|
|
224
94
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
95
|
+
- No shared remote or bare repo is needed. It exits 128 on failure, so
|
|
96
|
+
`&&` chaining is safe.
|
|
97
|
+
- Quote the URL. Unquoted, your local shell expands `~` to your laptop's
|
|
98
|
+
home.
|
|
99
|
+
- Bucket work by file cluster, not by machine.
|
|
229
100
|
|
|
230
|
-
|
|
231
|
-
evidence you asked for. What is unverified is the MERGE: the worker
|
|
232
|
-
forked from a base that has since moved, so the only new information is
|
|
233
|
-
in the combination.
|
|
101
|
+
### Run the merged gate on the host
|
|
234
102
|
|
|
235
|
-
|
|
236
|
-
head to the
|
|
103
|
+
Do not re-run what the worker ran. Only the merge is unverified.
|
|
104
|
+
Cherry-pick locally, push the merged head to the host, and run the gate
|
|
105
|
+
there with its warm dependencies:
|
|
237
106
|
|
|
238
107
|
```bash
|
|
239
108
|
git cherry-pick <sha>
|
|
240
109
|
git push -q "ssh://dev/~/hacking/<repo>.git" HEAD:refs/heads/main
|
|
241
|
-
ssh dev 'cd ~/hacking/<checkout> && git fetch -q origin \
|
|
242
|
-
&& git reset -q --hard origin/main && npm run check'
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
**On a session-capped host, run that gate through mule instead.** The
|
|
246
|
-
last line holds the channel for the whole suite — minutes — which is
|
|
247
|
-
precisely when your other tooling starts failing with a credentials
|
|
248
|
-
error that has nothing to do with credentials:
|
|
249
|
-
|
|
250
|
-
```bash
|
|
251
110
|
mule run --cwd ~/hacking/<checkout> --wait \
|
|
252
111
|
'git fetch -q origin && git reset -q --hard origin/main && npm run check'
|
|
253
112
|
```
|
|
254
113
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
114
|
+
`--wait` exits with the suite's code. A bare ssh would hold the capped
|
|
115
|
+
channel for minutes. Keep two gates local: platform-sensitive tests (macOS `ps` returns argv,
|
|
116
|
+
Linux `ps -e` appends the environment, and a real bug lived there), and
|
|
117
|
+
the final gate before the push that matters.
|
|
258
118
|
|
|
259
|
-
|
|
260
|
-
the worker used — so the marginal cost is near zero, while the same run
|
|
261
|
-
on a laptop is minutes of CPU per integration.
|
|
262
|
-
|
|
263
|
-
**Keep two things local.** A **platform-sensitive** subset, because a
|
|
264
|
-
remote green does not prove a local green when the bug is
|
|
265
|
-
platform-shaped: macOS `ps` returns argv where Linux `ps -e` appends the
|
|
266
|
-
environment, and a real bug lived in exactly that gap. And the **final**
|
|
267
|
-
gate before the push that matters, since that one is about your tree
|
|
268
|
-
rather than the worker's.
|
|
119
|
+
## Traps
|
|
269
120
|
|
|
270
|
-
|
|
121
|
+
### A spawn on an auth prompt looks healthy
|
|
271
122
|
|
|
272
|
-
|
|
123
|
+
With the channel busy, the attach ssh stopped at `Enter a passcode:`.
|
|
124
|
+
Spawn succeeded, the agent showed `needs_input`, and the prompt went
|
|
125
|
+
into the passcode field. Confirm a remote agent got the work
|
|
126
|
+
(`mu agent read <name> -n 20`) before trusting it. Send nothing
|
|
127
|
+
sensitive to an unconfirmed pane.
|
|
273
128
|
|
|
274
129
|
### Use the host's real CLI command
|
|
275
130
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
status, and an agent with **no models configured** — it looks fine
|
|
280
|
-
until you send it work. Check first:
|
|
281
|
-
|
|
282
|
-
```bash
|
|
283
|
-
ssh dev 'command -v pi-meta'
|
|
284
|
-
```
|
|
131
|
+
If your local `$MU_PI_COMMAND` is a wrapper such as `pi-meta
|
|
132
|
+
--pi-meta-no-solo --approve`, plain `pi` on the host gives a live pane
|
|
133
|
+
with **no models configured**. Check with `ssh dev 'command -v pi-meta'`.
|
|
285
134
|
|
|
286
135
|
### Tell the worker that mu is absent
|
|
287
136
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
worker print its sha instead; YOU claim, note and close from the
|
|
292
|
-
orchestrator. Omit this and the worker burns a turn on `command not
|
|
293
|
-
found`, or worse, silently fails to close and your `mu task wait`
|
|
294
|
-
hangs.
|
|
295
|
-
|
|
296
|
-
### Everything is orchestrator-PULL
|
|
137
|
+
mu is usually not on the host, so the in-pane loop cannot run. Say so
|
|
138
|
+
in the prompt and have the worker print its sha. You claim, note, and
|
|
139
|
+
close. Otherwise `mu task wait` hangs.
|
|
297
140
|
|
|
298
|
-
|
|
299
|
-
devserver typically has no route back to a NAT'd machine. So: you push
|
|
300
|
-
setup out, you fetch commits back. Never write a recipe in which the
|
|
301
|
-
host reaches you, and never assume a peer can `git fetch` from you.
|
|
141
|
+
### A dropped connection reaps the task, not the commit
|
|
302
142
|
|
|
303
|
-
|
|
143
|
+
The reaper reverts the task to OPEN, but the commit is still on the
|
|
144
|
+
host. Fetch from the `REMOTE:` path before re-dispatching.
|
|
304
145
|
|
|
305
|
-
|
|
306
|
-
pane dies with it, and mu behaves exactly as for a dead local agent:
|
|
307
|
-
the agent row goes, and the reaper reverts the task `IN_PROGRESS →
|
|
308
|
-
OPEN` with a `[reaper]` note. That is correct and desirable.
|
|
146
|
+
### A dead non-pi remote agent vanishes
|
|
309
147
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
148
|
+
murmur records `crashed` only when a pane outlives its process. Remotely
|
|
149
|
+
tmux reaps the session with the agent, so SIGKILL gives zero rows, not
|
|
150
|
+
`crashed`. (A dead pi agent probes `refused`.) Trust the `[reaper]`
|
|
151
|
+
note and `ssh <host> 'tmux ls'`.
|
|
314
152
|
|
|
315
|
-
###
|
|
153
|
+
### Stopping and cleaning up
|
|
316
154
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
collect.
|
|
155
|
+
- `mu agent abort <name>` stops a pi turn through the forwarded socket.
|
|
156
|
+
- `mu agent kick` signals the local ssh client, not the remote agent.
|
|
157
|
+
For an unresponsive agent, `mu agent close` and respawn.
|
|
158
|
+
- mu does not remove remote worktrees:
|
|
159
|
+
`ssh dev 'git -C ~/repo worktree remove ~/ws/worker-1'`.
|
|
323
160
|
|
|
324
|
-
|
|
325
|
-
zero crashed**, not a crash. Nothing distinguishes "it died" from "it
|
|
326
|
-
finished and I closed it". A second consequence of the same rule: the
|
|
327
|
-
crash path only fires for an agent killed **mid-turn**, since an idle
|
|
328
|
-
agent is already `stopped` and a stopped owner dying reads as a normal
|
|
329
|
-
finish.
|
|
161
|
+
## murmur
|
|
330
162
|
|
|
331
|
-
|
|
332
|
-
|
|
163
|
+
pi agents need no murmur. [murmur](https://github.com/mu-crew/murmur)
|
|
164
|
+
reports state for non-pi agents on tmux, and adds status pills,
|
|
165
|
+
attachment hints, and `murmur peer list`. Without it, non-pi agents
|
|
166
|
+
report `unknown`; everything else works.
|
|
333
167
|
|
|
334
|
-
|
|
335
|
-
|
|
168
|
+
| Question | Ask |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| What happens next, who owns it, is it done | mu: the DAG, `claim`, `task wait` |
|
|
171
|
+
| What is a pi agent doing | mu: the control socket |
|
|
172
|
+
| What is a non-pi agent doing; is anything blocked on me | murmur |
|
|
173
|
+
| Which host can I reach | murmur: `peer list` |
|
|
336
174
|
|
|
337
|
-
`
|
|
175
|
+
**`$MU_REMOTE_ENV` goes inside the ssh command.** Without it the agent
|
|
176
|
+
reports `driver=human` and shows in your own status bar.
|
|
338
177
|
|
|
339
|
-
|
|
178
|
+
**murmur sees the remote pane** and links it to your local pane by the
|
|
179
|
+
agent name in the local argv (`attached here %N`; focus instead of a
|
|
180
|
+
second connection). The detached shape carries the name only in the
|
|
181
|
+
session name, so name it `mu-<agent>`. An env prefix does not help:
|
|
182
|
+
macOS `ps` reports argv, not env.
|
|
340
183
|
|
|
341
|
-
|
|
342
|
-
exists:
|
|
184
|
+
### Picking a host
|
|
343
185
|
|
|
344
186
|
```bash
|
|
345
|
-
|
|
187
|
+
murmur peer list --json | jq -r '.[] | select(.error) | .name'
|
|
346
188
|
```
|
|
347
189
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
---
|
|
353
|
-
|
|
354
|
-
## mu and murmur, and what you lose without it
|
|
190
|
+
- `ssh` is last-known reachability. `error` is the current attempt.
|
|
191
|
+
- It exits 0 either way. Branch on `.error`, not on presence.
|
|
192
|
+
- `murmur collect` prints one line per unreachable host, now.
|
|
355
193
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
The DAG, claims, task completion waits, workspaces, spawn, send, read,
|
|
359
|
-
the reaper, this remote recipe, and `git fetch` collection still work.
|
|
360
|
-
Agent-state consumers such as `mu agent wait`, idle flags, and task-stall
|
|
361
|
-
detection wait rather than treating `unknown` as completion or a stall.
|
|
362
|
-
|
|
363
|
-
murmur also provides the tmux status pills, `prefix+a`, attachment hints,
|
|
364
|
-
and `murmur peer list` for host reachability. The division is: **mu owns
|
|
365
|
-
the work; murmur reports what an agent is doing.** murmur never places
|
|
366
|
-
work. Its interfaces used by mu are in murmur's
|
|
367
|
-
[stable contract](https://github.com/mu-crew/murmur/blob/main/ARCHITECTURE.md#contract).
|
|
368
|
-
|
|
369
|
-
| question | ask |
|
|
370
|
-
| --- | --- |
|
|
371
|
-
| what should happen next | mu — the DAG |
|
|
372
|
-
| who owns this task | mu — `claim` / `close` |
|
|
373
|
-
| is the task done | mu — `task wait`, a DB poll |
|
|
374
|
-
| what is this agent doing right now | murmur — pushed from inside pi |
|
|
375
|
-
| is anything blocked on me, anywhere | murmur |
|
|
376
|
-
| which host can I reach | murmur — `peer list` |
|
|
377
|
-
|
|
378
|
-
**The seam is three env vars, and it is load-bearing.** `mu agent
|
|
379
|
-
spawn` injects `MU_MANAGED_AGENT=1`, `MU_AGENT_NAME` and
|
|
380
|
-
`MU_WORKSTREAM`; pi inherits them and murmur's extension reads them.
|
|
381
|
-
That one mechanism gives you:
|
|
382
|
-
|
|
383
|
-
1. `driver=orchestrated`, so crew stays out of the human's status bar
|
|
384
|
-
unless it is blocked or crashed — true for local and remote alike
|
|
385
|
-
2. the workstream and agent name on the row, for grouping
|
|
386
|
-
3. the attachment back-reference for a remote worker
|
|
387
|
-
|
|
388
|
-
For a remote worker they go **inside** the ssh command (tmux `-e` stops
|
|
389
|
-
at the hop). Miss them and the agent reports `driver=human`: it appears
|
|
390
|
-
in your own counts as if it were yours.
|
|
391
|
-
|
|
392
|
-
## Picking a host
|
|
393
|
-
|
|
394
|
-
mu does not track hosts and should not; that is murmur's job. If it is
|
|
395
|
-
installed:
|
|
194
|
+
Attach with the peer's jump command instead of hardcoding ssh. It may
|
|
195
|
+
use a transport that takes no ssh session:
|
|
396
196
|
|
|
397
197
|
```bash
|
|
398
|
-
murmur peer
|
|
198
|
+
murmur peer set <name> --jump-command '<command with {pane}>'
|
|
199
|
+
JUMP=$(murmur peer list --json | jq -r '.[]|select(.name=="dev").jump_command')
|
|
200
|
+
mu agent spawn worker-1 -w big --command "${JUMP//\{pane\}/mu-worker-1}"
|
|
399
201
|
```
|
|
400
202
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
- `ssh` is **last-known** reachability; `error` is the **current**
|
|
404
|
-
attempt. A host can read `warm` and be failing right now.
|
|
405
|
-
- The command exits **0** either way — a fleet with sleeping machines
|
|
406
|
-
is the normal state, not a fault.
|
|
407
|
-
- So branch on `.error`, never on presence in the list.
|
|
408
|
-
|
|
409
|
-
`murmur status` and `pick` are polling paths and stay silent whatever
|
|
410
|
-
the fleet is doing. `murmur collect` is the deliberate dial: it prints
|
|
411
|
-
one line per host it could not reach, so it is the one to run for "can I
|
|
412
|
-
reach this host *right now*".
|
|
203
|
+
## When the host limits concurrent sessions
|
|
413
204
|
|
|
414
|
-
|
|
205
|
+
Most sshd allow 10 sessions per connection (`MaxSessions`). A hardened
|
|
206
|
+
host may set `MaxSessions 1`. Then the ssh holding your agent takes the
|
|
207
|
+
only channel, and every other ssh, including `git fetch`, fails with a
|
|
208
|
+
misleading `Permission denied (keyboard-interactive)`.
|
|
415
209
|
|
|
416
|
-
|
|
417
|
-
collector) and a jump command (for a HUMAN — need not be ssh):
|
|
210
|
+
Diagnose in this order. The first check is local and instant:
|
|
418
211
|
|
|
419
212
|
```bash
|
|
420
|
-
|
|
213
|
+
ssh-add -l # "Error connecting to agent" => dead ssh agent, NOT the cap
|
|
214
|
+
ssh <host> true # fails while `mu agent read` works => the cap
|
|
421
215
|
```
|
|
422
216
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
On a session-capped host this is the difference between holding the one
|
|
428
|
-
slot for your whole visit and holding nothing (see the ET section).
|
|
429
|
-
Don't hardcode `et` — a site wrapper may add VPN selection and its own
|
|
430
|
-
binary resolution, which is why the value is opaque.
|
|
431
|
-
|
|
432
|
-
---
|
|
217
|
+
A dead `SSH_AUTH_SOCK` gives the same error via an invisible 2FA prompt;
|
|
218
|
+
one session lost an hour treating it as the cap. To find what holds the
|
|
219
|
+
channel:
|
|
220
|
+
`ps -o pid=,command= -ax | grep "[s]sh <host>"`.
|
|
433
221
|
|
|
434
|
-
|
|
222
|
+
`ControlMaster no` does not help (it governs master creation only).
|
|
223
|
+
There is no client fix.
|
|
435
224
|
|
|
436
|
-
|
|
225
|
+
### Run commands through mule
|
|
437
226
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
the only channel, and every other ssh — including `git fetch` — is
|
|
442
|
-
refused with the misleading 2FA error described in § Never leave an
|
|
443
|
-
attach pane open.
|
|
227
|
+
[mule](https://github.com/mu-crew/mule) dispatches detached jobs on its
|
|
228
|
+
own ssh ControlPath. On a `MaxSessions 1` host, eight bare `rev-parse`
|
|
229
|
+
polls returned one sha and seven empty results; mule ran all eight.
|
|
444
230
|
|
|
445
|
-
|
|
446
|
-
|
|
231
|
+
- Use mule for long work and for any command whose refusal could look
|
|
232
|
+
like success. Keep worktree setup and `murmur collect` bare: they fail
|
|
233
|
+
loudly.
|
|
234
|
+
- A mule job is entirely remote. It cannot fetch from, push to, or rsync
|
|
235
|
+
with your laptop.
|
|
236
|
+
- For an interactive agent under mule's lifecycle, use `mule run --tui`
|
|
237
|
+
(plain `mule run` has no live TUI). It prints the attach command.
|
|
238
|
+
Closing that pane does not stop the job.
|
|
447
239
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
ssh
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
this, diagnosed as session exhaustion throughout.
|
|
240
|
+
| mule exit | Meaning | Do |
|
|
241
|
+
| --- | --- | --- |
|
|
242
|
+
| 3 | No ssh master; opening one may need a hardware-key touch | Hand back: ask the operator to run the printed command. Never retry, run `ssh -MNf`, or fall back to `ssh <host> <cmd>` |
|
|
243
|
+
| 4, 6 | Not yet | Poll again |
|
|
244
|
+
| 5 | No result will ever arrive | Stop polling |
|
|
454
245
|
|
|
455
|
-
|
|
246
|
+
None of these means the work failed. Full table: `mule --help`.
|
|
456
247
|
|
|
457
|
-
|
|
458
|
-
ssh-add -l # "Error connecting to agent" => dead agent, NOT the cap
|
|
459
|
-
ssh <host> true # fails while `mu agent read` works => the cap
|
|
460
|
-
```
|
|
248
|
+
### Run the agent in detached remote tmux
|
|
461
249
|
|
|
462
|
-
|
|
463
|
-
can capture**. That is what finally exposed it: the refusal looks like a
|
|
464
|
-
silent failure because the question is being asked somewhere with no
|
|
465
|
-
terminal attached.
|
|
466
|
-
|
|
467
|
-
`ControlMaster no` looks like it should help and does not. It governs
|
|
468
|
-
master *creation* only; a refused channel falls back regardless.
|
|
469
|
-
Measured with `no` set: three of four concurrent calls still produced
|
|
470
|
-
the misleading 2FA error. There is no client-side fix — `MaxSessions`
|
|
471
|
-
exists only in `sshd_config`, and the client discovers the cap only by
|
|
472
|
-
being refused.
|
|
473
|
-
|
|
474
|
-
### Fix: run commands through mule
|
|
475
|
-
|
|
476
|
-
[mule](https://github.com/mu-crew/mule) opens a separate ssh
|
|
477
|
-
ControlPath and dispatches detached jobs. On a `MaxSessions 1` host,
|
|
478
|
-
five concurrent bare calls produced **1 of 5** successes; mule produced
|
|
479
|
-
**5 of 5**. A multi-minute mule job also left a concurrent plain ssh
|
|
480
|
-
working.
|
|
481
|
-
|
|
482
|
-
Route commands by failure mode, not duration. Use mule for long work
|
|
483
|
-
and whenever refusal could look like success. Eight concurrent bare
|
|
484
|
-
`rev-parse` polls returned one sha and seven empty results; mule
|
|
485
|
-
dispatched all eight. Keep worktree setup and `murmur collect` bare
|
|
486
|
-
because they fail loudly. See `mule --help` and the relevant subcommand
|
|
487
|
-
help for flags, job control, warnings, and exit codes.
|
|
488
|
-
|
|
489
|
-
**A mule job must be entirely remote.** The host has no route back to
|
|
490
|
-
your laptop, so a local-endpoint `git fetch`, `git push`, or rsync
|
|
491
|
-
cannot run inside the job. Collect from the orchestrator as described
|
|
492
|
-
in § Everything is orchestrator-PULL.
|
|
493
|
-
|
|
494
|
-
**Use `mule run --tui` for an interactive/full-screen agent when you want
|
|
495
|
-
mule's timeout, runtime, transcript, and cleanup.** Plain `mule run` pipes
|
|
496
|
-
output into the artifact, so attaching finds the pane but no live TUI.
|
|
497
|
-
mule prints the exact murmur jump and `mu agent spawn --command` lines after
|
|
498
|
-
TUI dispatch. mu still needs that local attach pane for read/send/wait; closing
|
|
499
|
-
it detaches without stopping the mule job. Use plain detached remote tmux when
|
|
500
|
-
you need none of mule's artifacts or lifecycle controls.
|
|
501
|
-
|
|
502
|
-
#### mule exit 3 is a handback
|
|
503
|
-
|
|
504
|
-
A missing ssh master can require a human to touch a hardware key. Ask
|
|
505
|
-
the operator to run the command mule prints. Do not retry, run
|
|
506
|
-
`ssh -MNf` yourself, or fall back to `ssh <host> <command>`; each avoids
|
|
507
|
-
the required handback or recreates the capped-channel failure.
|
|
508
|
-
|
|
509
|
-
#### Exit 4 is "not yet"; exit 5 is "never"
|
|
510
|
-
|
|
511
|
-
Exit 4 and 6 mean poll again because the job may still complete. Exit 5
|
|
512
|
-
means no result will arrive. None means the work itself failed. Read the
|
|
513
|
-
full table in `mule --help`.
|
|
514
|
-
|
|
515
|
-
### Fix: detached remote tmux (for the agent itself)
|
|
516
|
-
|
|
517
|
-
Run the agent in its own tmux session on the host, and attach to
|
|
518
|
-
*that*. The agent's lifetime is then decoupled from the connection, so
|
|
519
|
-
the session can be released.
|
|
250
|
+
Decouple the agent from the connection to release the channel:
|
|
520
251
|
|
|
521
252
|
```bash
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
253
|
+
eval "$(mu agent remote-env worker-1 -w big --shell)"
|
|
254
|
+
# Agent runs detached on the host; this ssh returns at once
|
|
255
|
+
ssh dev "tmux new-session -d -s mu-worker-1 -c ~/ws/worker-1 \
|
|
256
|
+
'$MU_REMOTE_ENV \$SHELL -ilc \"pi --approve\"'"
|
|
525
257
|
|
|
526
|
-
# Attach a local pane
|
|
527
|
-
mu agent spawn worker-1 -w big --command
|
|
258
|
+
# Attach a local pane (with the socket forward); claim and send as usual
|
|
259
|
+
mu agent spawn worker-1 -w big --command "ssh $MU_SSH_ARGS dev -t 'tmux attach -t mu-worker-1'"
|
|
528
260
|
|
|
529
|
-
# COLLECT: detach
|
|
261
|
+
# COLLECT: detach first to free the channel, then fetch
|
|
530
262
|
mu agent close worker-1 -w big
|
|
531
263
|
git fetch "ssh://dev/~/ws/worker-1" HEAD && git cherry-pick FETCH_HEAD
|
|
532
264
|
|
|
533
|
-
# Reattach
|
|
534
|
-
mu agent spawn worker-1 -w big --command 'ssh dev -t "tmux attach -t mu-worker-1"'
|
|
265
|
+
# Reattach later with the same spawn command; LLM context is intact
|
|
535
266
|
```
|
|
536
267
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
accumulate orphaned remote sessions mu cannot see; `ssh dev 'tmux
|
|
547
|
-
ls'` is the only inventory.
|
|
548
|
-
|
|
549
|
-
This adds a THIRD address: local pane → ssh → remote tmux session →
|
|
550
|
-
agent pane. `kick` reaches only the first, `ssh dev 'tmux ls'` is the
|
|
551
|
-
only view of the third. Keep the session name equal to the agent name
|
|
552
|
-
— mu records neither, so it is the only handle tying them together.
|
|
268
|
+
- An attached pane blocks other ssh exactly like a direct one. Close it
|
|
269
|
+
before fetching, or use mule.
|
|
270
|
+
- `mu agent close` detaches. It does not stop the agent. To stop it:
|
|
271
|
+
`ssh dev 'tmux kill-session -t mu-worker-1'`. `ssh dev 'tmux ls'` is
|
|
272
|
+
the only inventory of these sessions.
|
|
273
|
+
- A dropped connection no longer reaps the agent. Each attach
|
|
274
|
+
re-creates the socket forward.
|
|
275
|
+
- Keep the session name `mu-<agent>`: mu records neither, so it is the
|
|
276
|
+
only link. On an uncapped host this shape buys nothing.
|
|
553
277
|
|
|
554
|
-
|
|
555
|
-
attachment by spotting the agent name in the local pane's argv, and
|
|
556
|
-
with this shape the session name is the only place it appears. `-s
|
|
557
|
-
mu-worker-1` for agent `worker-1` gives you `attached here %N` on the
|
|
558
|
-
remote row; `-s scratch` silently does not.
|
|
278
|
+
### Use ET for your own shell
|
|
559
279
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
Use this shape only where you need it. On an ordinary host it is
|
|
565
|
-
pointless indirection.
|
|
566
|
-
|
|
567
|
-
### For your own interactive work, use ET instead
|
|
568
|
-
|
|
569
|
-
The recipe above is for mu agents, which need a pane whose process mu
|
|
570
|
-
controls. Your own shell on the host has an easier answer: **Eternal
|
|
571
|
-
Terminal holds no ssh session at all.** It bootstraps over ssh and then
|
|
572
|
-
hands off to `etserver` on its own transport, so `MaxSessions` never
|
|
573
|
-
counts it.
|
|
280
|
+
Eternal Terminal moves off ssh to `etserver`, so `MaxSessions` never
|
|
281
|
+
counts it, even with a nested `tmux attach`. It makes a good jump
|
|
282
|
+
command but cannot serve `git fetch`.
|
|
574
283
|
|
|
575
284
|
```bash
|
|
576
|
-
et dev # or your site's wrapper,
|
|
285
|
+
et dev # or your site's wrapper, such as `x2ssh -et dev`
|
|
577
286
|
```
|
|
578
287
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
starved every other ssh for as long as it stayed open, while an ET
|
|
585
|
-
session left `ssh dev true` succeeding throughout — and the same held
|
|
586
|
-
with a nested `tmux attach` live inside it, with a concurrent `murmur
|
|
587
|
-
collect` reaching the host. So ET costs the capped slot nothing even
|
|
588
|
-
while you are sitting in a remote agent, which is what makes it usable
|
|
589
|
-
as a murmur jump command.
|
|
590
|
-
|
|
591
|
-
So the clean split on a capped host is ET for you, one `ssh -MNf <host>`
|
|
592
|
-
master for tooling, and detached tmux for mu agents. The three do not
|
|
593
|
-
compete.
|
|
594
|
-
|
|
595
|
-
Note ET cannot serve murmur or `git fetch` — it exposes no multiplexing
|
|
596
|
-
socket to attach to. That is exactly why it pairs well: it takes none of
|
|
597
|
-
the capped slots those tools need.
|
|
288
|
+
An aborted `et` or `x2ssh` leaves local processes behind. Run
|
|
289
|
+
`pkill -f x2ssh` before retrying.
|
|
290
|
+
|
|
291
|
+
On a capped host: ET for you, mule for tooling, detached tmux for mu
|
|
292
|
+
agents. None of them competes for the slot.
|