@mu-crew/mu 3.6.1 → 3.7.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/dist/cli.js +217 -120
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +37 -10
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +27 -4
- package/dist/index.js +136 -58
- package/dist/index.js.map +1 -1
- package/docs/architecture/control-socket.md +2 -2
- package/docs/architecture/mux.md +4 -1
- package/docs/guide/delegate.md +7 -3
- package/docs/guide/dispatch.md +6 -3
- package/docs/guide/getting-started.md +1 -1
- package/docs/guide/recovery.md +11 -6
- package/docs/guide/upgrade.md +4 -4
- package/docs/reference/env.md +1 -1
- package/package.json +1 -1
- package/skills/mu/SKILL.md +6 -3
- package/skills/mu/recipes/codemode-driver.md +2 -2
- package/skills/mu/recipes/orchestrator-loop.md +10 -6
- package/skills/mu/recipes/remote-workers.md +23 -6
- package/skills/mu/recipes/tasks-or-calls.md +6 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Database } from 'better-sqlite3';
|
|
2
2
|
|
|
3
|
-
type CtlState = "busy" | "idle"
|
|
3
|
+
type CtlState = "busy" | "idle";
|
|
4
4
|
/** Session commands the `command` op runs: `/new`, `/reload`, `/compact`. */
|
|
5
5
|
declare const CTL_COMMANDS: readonly ["new", "reload", "compact"];
|
|
6
6
|
type CtlCommandName = (typeof CTL_COMMANDS)[number];
|
|
@@ -17,6 +17,11 @@ interface StateReading {
|
|
|
17
17
|
reason?: string;
|
|
18
18
|
/** Set for agents that expect a control socket. */
|
|
19
19
|
ctl?: Exclude<CtlLink, "n/a">;
|
|
20
|
+
/** ctl only: pi holds queued messages, so an idle reading is about to
|
|
21
|
+
* turn busy. */
|
|
22
|
+
pending?: boolean;
|
|
23
|
+
/** ctl only: pi's settled-run count (`mu agent wait --after-runs`). */
|
|
24
|
+
runs?: number;
|
|
20
25
|
}
|
|
21
26
|
interface StateAgentRef {
|
|
22
27
|
name: string;
|
|
@@ -1460,7 +1465,10 @@ type SpawnedAgent = AgentRow & {
|
|
|
1460
1465
|
* 4. setPaneTitle + enableMuPaneBordersForPane.
|
|
1461
1466
|
* 5. finalizeAgentRow() — patch placeholder pane_id to real (workspace
|
|
1462
1467
|
* path), or insert a fresh agent row (no-workspace path).
|
|
1463
|
-
* 6. awaitSpawnLiveness()
|
|
1468
|
+
* 6. Liveness: awaitSpawnLiveness() (fixed sleep, then pane + scan
|
|
1469
|
+
* check), or for a tmux pi agent the ctl handshake with a pane
|
|
1470
|
+
* check per tick and the scan at the end. herdr: startAgentInPane.
|
|
1471
|
+
* 7. ctl handshake for herdr pi agents (outside the rollback).
|
|
1464
1472
|
*
|
|
1465
1473
|
* Failure between any of (3)–(6) calls rollbackSpawn() to undo the
|
|
1466
1474
|
* pane + row + workspace. The caller-visible error is preserved.
|
|
@@ -1645,7 +1653,9 @@ declare class AgentDiedOnSpawnError extends Error implements HasNextSteps {
|
|
|
1645
1653
|
readonly remote: boolean;
|
|
1646
1654
|
constructor(agentName: string, paneId: string, scrollback: string | undefined,
|
|
1647
1655
|
/** The command the pane ran; an `ssh ...` command gets remote hints. */
|
|
1648
|
-
command?: string | undefined
|
|
1656
|
+
command?: string | undefined,
|
|
1657
|
+
/** When it died; default `within <MU_SPAWN_LIVENESS_MS>ms of spawn`. */
|
|
1658
|
+
when?: string);
|
|
1649
1659
|
errorNextSteps(): NextStep[];
|
|
1650
1660
|
}
|
|
1651
1661
|
/**
|
|
@@ -1691,7 +1701,9 @@ declare class AgentSpawnStartupError extends Error implements HasNextSteps {
|
|
|
1691
1701
|
matchedLine: string,
|
|
1692
1702
|
/** Full captured scrollback (tail-trimmed already by
|
|
1693
1703
|
* awaitSpawnLiveness). Attached to the message for context. */
|
|
1694
|
-
scrollback: string
|
|
1704
|
+
scrollback: string,
|
|
1705
|
+
/** When it was seen; default `within <MU_SPAWN_LIVENESS_MS>ms of spawn`. */
|
|
1706
|
+
when?: string);
|
|
1695
1707
|
errorNextSteps(): NextStep[];
|
|
1696
1708
|
}
|
|
1697
1709
|
/**
|
|
@@ -1918,6 +1930,13 @@ type Transport = "ctl" | "mux";
|
|
|
1918
1930
|
type SendResult = {
|
|
1919
1931
|
transport: Transport;
|
|
1920
1932
|
state?: CtlState;
|
|
1933
|
+
/**
|
|
1934
|
+
* ctl only: pi's completed-run count. For a plain send it is measured
|
|
1935
|
+
* before dispatch (the `mu agent wait --after-runs` baseline); for
|
|
1936
|
+
* `--fresh` it is the new session's count once its run started.
|
|
1937
|
+
* Absent from an extension that predates it.
|
|
1938
|
+
*/
|
|
1939
|
+
runs?: number;
|
|
1921
1940
|
/** Set when the text ran as a pi session command over ctl. */
|
|
1922
1941
|
command?: CtlCommandName;
|
|
1923
1942
|
};
|
|
@@ -3437,6 +3456,10 @@ interface TaskWaitOptions {
|
|
|
3437
3456
|
* observation-only (wait keeps polling); `onStall: 'exit'` makes it
|
|
3438
3457
|
* terminal. */
|
|
3439
3458
|
stuckAfterMs?: number;
|
|
3459
|
+
/** `stuckAfterMs` for an owner whose reading came from a working
|
|
3460
|
+
* control socket (source ctl, ctl ok). Default
|
|
3461
|
+
* `DEFAULT_CTL_STUCK_AFTER_MS`. `stuckAfterMs: 0` disables both. */
|
|
3462
|
+
ctlStuckAfterMs?: number;
|
|
3440
3463
|
/** What to do when the `--stuck-after` predicate fires on a watched
|
|
3441
3464
|
* task. `'warn'` (default) = today's behaviour: yellow STUCK line
|
|
3442
3465
|
* to stderr (deduped per task per wait call) + corroborating
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { execa as execa3 } from "execa";
|
|
|
12
12
|
|
|
13
13
|
// src/agents/transport.ts
|
|
14
14
|
import { existsSync as existsSync8 } from "fs";
|
|
15
|
-
import { dirname as
|
|
15
|
+
import { dirname as dirname7 } from "path";
|
|
16
16
|
|
|
17
17
|
// src/ctl/client.ts
|
|
18
18
|
import { createConnection } from "net";
|
|
@@ -144,6 +144,7 @@ async function ctlProbe(sock, timeoutMs = CTL_DEFAULT_TIMEOUT_MS) {
|
|
|
144
144
|
|
|
145
145
|
// src/ctl/path.ts
|
|
146
146
|
import { createHash } from "crypto";
|
|
147
|
+
import { rmSync } from "fs";
|
|
147
148
|
import { dirname as dirname2, join as join2 } from "path";
|
|
148
149
|
|
|
149
150
|
// src/db.ts
|
|
@@ -854,6 +855,13 @@ function ctlSocketPath(workstream, agent, stateDir) {
|
|
|
854
855
|
const base = stateDir ?? (dbPath ? dirname2(dbPath) : defaultStateDir());
|
|
855
856
|
return sockPathUnder(join2(base, "sock"), workstream, agent);
|
|
856
857
|
}
|
|
858
|
+
function unlinkCtlSocket(db, name, workstream) {
|
|
859
|
+
if (db.memory) return;
|
|
860
|
+
try {
|
|
861
|
+
rmSync(ctlSocketPath(workstream, name, dirname2(db.name)), { force: true });
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
}
|
|
857
865
|
|
|
858
866
|
// src/mux.ts
|
|
859
867
|
var mux_exports = {};
|
|
@@ -2254,13 +2262,12 @@ function resetMux() {
|
|
|
2254
2262
|
// src/agents/spawn.ts
|
|
2255
2263
|
import { execFile as execFile2 } from "child_process";
|
|
2256
2264
|
import { mkdirSync as mkdirSync2, rmSync as rmSync4 } from "fs";
|
|
2257
|
-
import { basename, dirname as
|
|
2265
|
+
import { basename, dirname as dirname6 } from "path";
|
|
2258
2266
|
import { setTimeout as delay } from "timers/promises";
|
|
2259
2267
|
import { promisify as promisify2 } from "util";
|
|
2260
2268
|
|
|
2261
2269
|
// src/agents.ts
|
|
2262
|
-
import {
|
|
2263
|
-
import { dirname as dirname4 } from "path";
|
|
2270
|
+
import { dirname as dirname5 } from "path";
|
|
2264
2271
|
|
|
2265
2272
|
// src/glyphs.ts
|
|
2266
2273
|
var AGENT_STATE_GLYPH = {
|
|
@@ -2856,7 +2863,7 @@ function workspacesRoot(workstream) {
|
|
|
2856
2863
|
}
|
|
2857
2864
|
|
|
2858
2865
|
// src/workspace/crud.ts
|
|
2859
|
-
import { existsSync as existsSync6, rmSync as
|
|
2866
|
+
import { existsSync as existsSync6, rmSync as rmSync3 } from "fs";
|
|
2860
2867
|
import { homedir as homedir2 } from "os";
|
|
2861
2868
|
import { resolve as resolve3 } from "path";
|
|
2862
2869
|
|
|
@@ -2866,7 +2873,7 @@ import { resolve as resolve2 } from "path";
|
|
|
2866
2873
|
|
|
2867
2874
|
// src/vcs/helpers.ts
|
|
2868
2875
|
import { execFile } from "child_process";
|
|
2869
|
-
import { existsSync, rmSync } from "fs";
|
|
2876
|
+
import { existsSync, rmSync as rmSync2 } from "fs";
|
|
2870
2877
|
import { mkdir } from "fs/promises";
|
|
2871
2878
|
import { dirname as dirname3 } from "path";
|
|
2872
2879
|
import { promisify } from "util";
|
|
@@ -2972,7 +2979,7 @@ async function ensureParent(path) {
|
|
|
2972
2979
|
}
|
|
2973
2980
|
function rmDirSync(path) {
|
|
2974
2981
|
if (!existsSync(path)) return false;
|
|
2975
|
-
|
|
2982
|
+
rmSync2(path, { recursive: true, force: true });
|
|
2976
2983
|
return true;
|
|
2977
2984
|
}
|
|
2978
2985
|
async function run(bin, args, cwd) {
|
|
@@ -3797,7 +3804,7 @@ async function createWorkspace(db, opts) {
|
|
|
3797
3804
|
created = await backend.createWorkspace(createOpts);
|
|
3798
3805
|
} catch (err) {
|
|
3799
3806
|
try {
|
|
3800
|
-
|
|
3807
|
+
rmSync3(path, { recursive: true, force: true });
|
|
3801
3808
|
} catch {
|
|
3802
3809
|
}
|
|
3803
3810
|
throw err;
|
|
@@ -4438,8 +4445,17 @@ var CycleError = class extends Error {
|
|
|
4438
4445
|
];
|
|
4439
4446
|
}
|
|
4440
4447
|
};
|
|
4448
|
+
function stallWaitHint(owner, ws, runs) {
|
|
4449
|
+
if (runs === void 0 || runs < 1) return [];
|
|
4450
|
+
return [
|
|
4451
|
+
{
|
|
4452
|
+
intent: "Read the worker's last answer (its final text, at once)",
|
|
4453
|
+
command: `mu agent wait ${owner} --after-runs ${runs - 1} --json -w ${ws}`
|
|
4454
|
+
}
|
|
4455
|
+
];
|
|
4456
|
+
}
|
|
4441
4457
|
var StallDetectedDuringWaitError = class extends Error {
|
|
4442
|
-
constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false) {
|
|
4458
|
+
constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false, ownerRuns) {
|
|
4443
4459
|
const ownerBit = owner !== null ? owner : "<unknown>";
|
|
4444
4460
|
super(
|
|
4445
4461
|
`task ${taskName} owned by ${ownerBit} has been needs_input for ${ageSecs}s; exiting per --on-stall exit. Read the pane to see which it is \u2014 finished without closing, waiting on an answer, or sitting at a prompt.`
|
|
@@ -4449,12 +4465,14 @@ var StallDetectedDuringWaitError = class extends Error {
|
|
|
4449
4465
|
this.workstream = workstream;
|
|
4450
4466
|
this.ageSecs = ageSecs;
|
|
4451
4467
|
this.ownerExpectsCtl = ownerExpectsCtl;
|
|
4468
|
+
this.ownerRuns = ownerRuns;
|
|
4452
4469
|
}
|
|
4453
4470
|
taskName;
|
|
4454
4471
|
owner;
|
|
4455
4472
|
workstream;
|
|
4456
4473
|
ageSecs;
|
|
4457
4474
|
ownerExpectsCtl;
|
|
4475
|
+
ownerRuns;
|
|
4458
4476
|
name = "StallDetectedDuringWaitError";
|
|
4459
4477
|
/**
|
|
4460
4478
|
* Diagnose before acting. `mu agent read` comes FIRST because the
|
|
@@ -4469,6 +4487,7 @@ var StallDetectedDuringWaitError = class extends Error {
|
|
|
4469
4487
|
const ws = this.workstream;
|
|
4470
4488
|
const ownerBit = this.owner !== null ? this.owner : "<owner>";
|
|
4471
4489
|
return [
|
|
4490
|
+
...stallWaitHint(ownerBit, ws, this.ownerRuns),
|
|
4472
4491
|
{
|
|
4473
4492
|
intent: "Read the worker's pane to see what it is waiting on",
|
|
4474
4493
|
command: `mu agent read ${ownerBit} -w ${ws} --lines 60`
|
|
@@ -4822,7 +4841,7 @@ function reparentTaskImpl(db, taskLocalId, blockers, scope) {
|
|
|
4822
4841
|
|
|
4823
4842
|
// src/workstream.ts
|
|
4824
4843
|
import { existsSync as existsSync7, readdirSync as readdirSync2, rmdirSync } from "fs";
|
|
4825
|
-
import { join as join5 } from "path";
|
|
4844
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
4826
4845
|
var WORKSTREAM_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
4827
4846
|
var RESERVED_WORKSTREAM_PREFIX = "mu-";
|
|
4828
4847
|
var RESERVED_WORKSTREAM_NAMES = /* @__PURE__ */ new Set(["scratch"]);
|
|
@@ -4936,6 +4955,7 @@ async function teardownWorkstream(db, opts) {
|
|
|
4936
4955
|
const notesBefore = countNotes(db, opts.workstream);
|
|
4937
4956
|
const edgesBefore = countEdges(db, opts.workstream);
|
|
4938
4957
|
const workspacesBefore = listWorkspaces2(db, opts.workstream);
|
|
4958
|
+
const agentNames = listAgentNames(db, opts.workstream);
|
|
4939
4959
|
const mux = await activeMux();
|
|
4940
4960
|
const muxAliveBefore = await mux.sessionExists(muxSession);
|
|
4941
4961
|
if (muxAliveBefore) {
|
|
@@ -4973,6 +4993,14 @@ async function teardownWorkstream(db, opts) {
|
|
|
4973
4993
|
} catch {
|
|
4974
4994
|
}
|
|
4975
4995
|
}
|
|
4996
|
+
for (const name of agentNames) unlinkCtlSocket(db, name, opts.workstream);
|
|
4997
|
+
if (!db.memory) {
|
|
4998
|
+
const sockDir = join5(dirname4(db.name), "sock", opts.workstream);
|
|
4999
|
+
try {
|
|
5000
|
+
if (readdirSync2(sockDir).length === 0) rmdirSync(sockDir);
|
|
5001
|
+
} catch {
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
4976
5004
|
withOpContext(
|
|
4977
5005
|
db,
|
|
4978
5006
|
{ intent: "workstream.teardown", group: "new" },
|
|
@@ -4989,6 +5017,14 @@ async function teardownWorkstream(db, opts) {
|
|
|
4989
5017
|
failedWorkspaces
|
|
4990
5018
|
};
|
|
4991
5019
|
}
|
|
5020
|
+
function listAgentNames(db, workstream) {
|
|
5021
|
+
const rows = db.prepare(
|
|
5022
|
+
`SELECT a.name FROM agents a
|
|
5023
|
+
JOIN workstreams ws ON ws.id = a.workstream_id
|
|
5024
|
+
WHERE ws.name = ?`
|
|
5025
|
+
).all(workstream);
|
|
5026
|
+
return rows.map((r) => r.name);
|
|
5027
|
+
}
|
|
4992
5028
|
function countAgents(db, workstream) {
|
|
4993
5029
|
const row = db.prepare(
|
|
4994
5030
|
`SELECT COUNT(*) AS n FROM agents a
|
|
@@ -5542,6 +5578,7 @@ async function claimSelf(db, localId, opts) {
|
|
|
5542
5578
|
var currentWaitSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
5543
5579
|
var pollCount = 0;
|
|
5544
5580
|
var DEFAULT_STUCK_AFTER_MS = 12e4;
|
|
5581
|
+
var DEFAULT_CTL_STUCK_AFTER_MS = 5e3;
|
|
5545
5582
|
var defaultStuckWarn = (msg) => {
|
|
5546
5583
|
process.stderr.write(msg);
|
|
5547
5584
|
};
|
|
@@ -5588,6 +5625,7 @@ async function waitForTasks(db, input, opts) {
|
|
|
5588
5625
|
const timeoutMs = opts.timeoutMs ?? 6e5;
|
|
5589
5626
|
const pollMs = opts.pollMs ?? 1e3;
|
|
5590
5627
|
const stuckAfterMs = opts.stuckAfterMs ?? DEFAULT_STUCK_AFTER_MS;
|
|
5628
|
+
const ctlStuckAfterMs = opts.ctlStuckAfterMs ?? DEFAULT_CTL_STUCK_AFTER_MS;
|
|
5591
5629
|
const onStall = opts.onStall ?? "warn";
|
|
5592
5630
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : Number.POSITIVE_INFINITY;
|
|
5593
5631
|
for (const ref of refs) {
|
|
@@ -5599,12 +5637,15 @@ async function waitForTasks(db, input, opts) {
|
|
|
5599
5637
|
const ctlBrokenSince = /* @__PURE__ */ new Map();
|
|
5600
5638
|
const stuckLabel = /* @__PURE__ */ new Map();
|
|
5601
5639
|
const ctlOwners = /* @__PURE__ */ new Set();
|
|
5640
|
+
const ownerRuns = /* @__PURE__ */ new Map();
|
|
5602
5641
|
const stuckAgeMs = async (status, owner, workstreamName) => {
|
|
5603
5642
|
if (stuckAfterMs <= 0 || status !== "IN_PROGRESS" || owner === null || opts.readOwnerState === void 0)
|
|
5604
5643
|
return null;
|
|
5605
5644
|
const reading = await opts.readOwnerState({ name: owner, workstreamName });
|
|
5606
5645
|
const ownerKey = `${workstreamName}/${owner}`;
|
|
5607
5646
|
if (reading?.ctl !== void 0) ctlOwners.add(ownerKey);
|
|
5647
|
+
if (reading?.runs !== void 0) ownerRuns.set(ownerKey, reading.runs);
|
|
5648
|
+
else ownerRuns.delete(ownerKey);
|
|
5608
5649
|
if (reading?.alive === true && (reading.ctl === "missing" || reading.ctl === "refused")) {
|
|
5609
5650
|
const firstSeen = ctlBrokenSince.get(ownerKey) ?? Date.now();
|
|
5610
5651
|
ctlBrokenSince.set(ownerKey, firstSeen);
|
|
@@ -5615,8 +5656,10 @@ async function waitForTasks(db, input, opts) {
|
|
|
5615
5656
|
ctlBrokenSince.delete(ownerKey);
|
|
5616
5657
|
stuckLabel.delete(ownerKey);
|
|
5617
5658
|
if (reading?.state !== "needs_input" || reading.since === null) return null;
|
|
5659
|
+
const viaCtl = reading.source === "ctl" && reading.ctl === "ok";
|
|
5660
|
+
if (viaCtl && reading.pending === true) return null;
|
|
5618
5661
|
const ageMs = Date.now() - reading.since;
|
|
5619
|
-
return ageMs >= stuckAfterMs ? ageMs : null;
|
|
5662
|
+
return ageMs >= (viaCtl ? ctlStuckAfterMs : stuckAfterMs) ? ageMs : null;
|
|
5620
5663
|
};
|
|
5621
5664
|
const snapshot2 = async () => {
|
|
5622
5665
|
const refStates = [];
|
|
@@ -5632,10 +5675,13 @@ async function waitForTasks(db, input, opts) {
|
|
|
5632
5675
|
stuckWarned.add(key);
|
|
5633
5676
|
const ageSecs = Math.round(ageMs / 1e3);
|
|
5634
5677
|
const ownerBit = owner ?? "<none>";
|
|
5678
|
+
const runs = ownerRuns.get(`${ref.workstreamName}/${ownerBit}`);
|
|
5635
5679
|
const stateBit = stuckLabel.get(`${ref.workstreamName}/${ownerBit}`) ?? "needs_input";
|
|
5680
|
+
const waitLine = stallWaitHint(ownerBit, ref.workstreamName, runs).map((s) => ` ${s.command}
|
|
5681
|
+
`).join("");
|
|
5636
5682
|
currentStuckWarn(
|
|
5637
5683
|
`\x1B[33mmu task wait: ${key} needs attention \u2014 owner=${ownerBit} has been in ${stateBit} for ${formatStallAge(ageMs)}. It may have finished without closing, be waiting on an answer, or be sitting at a prompt.\x1B[0m
|
|
5638
|
-
mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
|
|
5684
|
+
` + waitLine + ` mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
|
|
5639
5685
|
`
|
|
5640
5686
|
);
|
|
5641
5687
|
emitEvent(
|
|
@@ -5651,7 +5697,8 @@ async function waitForTasks(db, input, opts) {
|
|
|
5651
5697
|
owner,
|
|
5652
5698
|
ref.workstreamName,
|
|
5653
5699
|
ageSecs,
|
|
5654
|
-
ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`)
|
|
5700
|
+
ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`),
|
|
5701
|
+
runs
|
|
5655
5702
|
);
|
|
5656
5703
|
}
|
|
5657
5704
|
}
|
|
@@ -6085,13 +6132,6 @@ function deleteAgent(db, name, workstream) {
|
|
|
6085
6132
|
if (deleted) unlinkCtlSocket(db, name, workstream);
|
|
6086
6133
|
return deleted;
|
|
6087
6134
|
}
|
|
6088
|
-
function unlinkCtlSocket(db, name, workstream) {
|
|
6089
|
-
if (db.memory) return;
|
|
6090
|
-
try {
|
|
6091
|
-
rmSync3(ctlSocketPath(workstream, name, dirname4(db.name)), { force: true });
|
|
6092
|
-
} catch {
|
|
6093
|
-
}
|
|
6094
|
-
}
|
|
6095
6135
|
function deleteAgentRow(db, name, workstream) {
|
|
6096
6136
|
return withOpContext(
|
|
6097
6137
|
db,
|
|
@@ -6187,7 +6227,7 @@ async function listLiveAgents(db, opts) {
|
|
|
6187
6227
|
...opts.mode !== void 0 ? { mode: opts.mode } : {}
|
|
6188
6228
|
});
|
|
6189
6229
|
const baseAgents = listAgents(db, { workstream: opts.workstream });
|
|
6190
|
-
const readings = await readAgentStates(baseAgents, { stateDir:
|
|
6230
|
+
const readings = await readAgentStates(baseAgents, { stateDir: dirname5(db.name) });
|
|
6191
6231
|
const now = Date.now();
|
|
6192
6232
|
const agents = baseAgents.map((agent) => {
|
|
6193
6233
|
const reading = readings.get(agentKey(agent)) ?? {
|
|
@@ -6452,7 +6492,7 @@ function defaultSpawnCtlMs() {
|
|
|
6452
6492
|
return parsed;
|
|
6453
6493
|
}
|
|
6454
6494
|
var CTL_POLL_INTERVAL_MS = 250;
|
|
6455
|
-
async function awaitCtlHandshake(sock) {
|
|
6495
|
+
async function awaitCtlHandshake(sock, onMiss) {
|
|
6456
6496
|
const budgetMs = defaultSpawnCtlMs();
|
|
6457
6497
|
const deadline = Date.now() + budgetMs;
|
|
6458
6498
|
for (; ; ) {
|
|
@@ -6460,6 +6500,7 @@ async function awaitCtlHandshake(sock) {
|
|
|
6460
6500
|
const probe = await ctlProbe(sock, Math.max(1, Math.min(remaining, CTL_POLL_INTERVAL_MS * 4)));
|
|
6461
6501
|
if (probe.kind === "ok") return "ok";
|
|
6462
6502
|
const last = probeToSpawnCtl(probe.kind);
|
|
6503
|
+
if (onMiss) await onMiss();
|
|
6463
6504
|
const left = deadline - Date.now();
|
|
6464
6505
|
if (left <= 0) return last;
|
|
6465
6506
|
await delay(Math.min(CTL_POLL_INTERVAL_MS, left));
|
|
@@ -6487,8 +6528,8 @@ async function spawnAgent(db, opts) {
|
|
|
6487
6528
|
}
|
|
6488
6529
|
}
|
|
6489
6530
|
const workspacePathStr = opts.workspace ? await prestageWorkspace(db, opts, cli) : void 0;
|
|
6490
|
-
const ctlSocket = ctlSocketPath(opts.workstream, opts.name,
|
|
6491
|
-
mkdirSync2(
|
|
6531
|
+
const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname6(db.name));
|
|
6532
|
+
mkdirSync2(dirname6(ctlSocket), { recursive: true, mode: 448 });
|
|
6492
6533
|
rmSync4(ctlSocket, { force: true });
|
|
6493
6534
|
const paneEnv = {
|
|
6494
6535
|
MU_MANAGED_AGENT: "1",
|
|
@@ -6499,6 +6540,8 @@ async function spawnAgent(db, opts) {
|
|
|
6499
6540
|
const hasWorkspace = workspacePathStr !== void 0;
|
|
6500
6541
|
let paneId;
|
|
6501
6542
|
let agent;
|
|
6543
|
+
const wantsCtl = opts.ctl !== false && speaksMuCtl(cli, command) && defaultSpawnCtlMs() !== 0;
|
|
6544
|
+
let ctl;
|
|
6502
6545
|
try {
|
|
6503
6546
|
const created = await withSpawnLock(session, async () => {
|
|
6504
6547
|
const pid = await createOrReusePane({
|
|
@@ -6519,7 +6562,23 @@ async function spawnAgent(db, opts) {
|
|
|
6519
6562
|
agent = created.row;
|
|
6520
6563
|
const startAgent = (await activeMux()).startAgentInPane;
|
|
6521
6564
|
if (startAgent === void 0) {
|
|
6522
|
-
|
|
6565
|
+
if (wantsCtl) {
|
|
6566
|
+
const pid = paneId;
|
|
6567
|
+
const check = defaultSpawnLivenessMs() !== 0;
|
|
6568
|
+
const when = `during the ctl handshake (up to ${defaultSpawnCtlMs()}ms after spawn)`;
|
|
6569
|
+
let lastScrollback;
|
|
6570
|
+
const assertPaneAlive = async () => {
|
|
6571
|
+
const mux = await activeMux();
|
|
6572
|
+
const sb = await mux.capturePane(pid, { lines: 50 }).catch(() => void 0);
|
|
6573
|
+
lastScrollback = sb ?? lastScrollback;
|
|
6574
|
+
if (await mux.paneExists(pid)) return;
|
|
6575
|
+
throw new AgentDiedOnSpawnError(opts.name, pid, lastScrollback, command, when);
|
|
6576
|
+
};
|
|
6577
|
+
ctl = await awaitCtlHandshake(ctlSocket, check ? assertPaneAlive : void 0);
|
|
6578
|
+
if (check) await checkSpawnHealth(pid, opts.name, command, when);
|
|
6579
|
+
} else {
|
|
6580
|
+
await awaitSpawnLiveness(paneId, opts.name, command);
|
|
6581
|
+
}
|
|
6523
6582
|
} else {
|
|
6524
6583
|
await startAgent({ paneId, name: opts.name, cli, command, commandSource });
|
|
6525
6584
|
}
|
|
@@ -6528,14 +6587,13 @@ async function spawnAgent(db, opts) {
|
|
|
6528
6587
|
if (hasWorkspace) attachOrphanCleanupHint(err, opts.name, opts.workstream);
|
|
6529
6588
|
throw err;
|
|
6530
6589
|
}
|
|
6531
|
-
|
|
6590
|
+
ctl ??= wantsCtl ? await awaitCtlHandshake(ctlSocket) : "skipped";
|
|
6532
6591
|
emitEvent(
|
|
6533
6592
|
db,
|
|
6534
6593
|
opts.workstream,
|
|
6535
6594
|
"agent.spawn",
|
|
6536
6595
|
`agent spawn ${opts.name} (cli=${cli}, role=${opts.role ?? "full-access"}, pane=${paneId})`
|
|
6537
6596
|
);
|
|
6538
|
-
await refreshAgentTitle(db, opts.name, opts.workstream);
|
|
6539
6597
|
maybeWarnNonConventionalAgentName(opts.name);
|
|
6540
6598
|
return { ...agent, ctl, ctlSocket };
|
|
6541
6599
|
}
|
|
@@ -6663,15 +6721,18 @@ async function awaitSpawnLiveness(paneId, agentName, command) {
|
|
|
6663
6721
|
const ms = defaultSpawnLivenessMs();
|
|
6664
6722
|
if (ms === 0) return;
|
|
6665
6723
|
await sleep(ms);
|
|
6724
|
+
await checkSpawnHealth(paneId, agentName, command);
|
|
6725
|
+
}
|
|
6726
|
+
async function checkSpawnHealth(paneId, agentName, command, when) {
|
|
6666
6727
|
const mux = await activeMux();
|
|
6667
6728
|
const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
|
|
6668
6729
|
if (!await mux.paneExists(paneId)) {
|
|
6669
|
-
throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command);
|
|
6730
|
+
throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when);
|
|
6670
6731
|
}
|
|
6671
6732
|
if (scrollback !== void 0) {
|
|
6672
6733
|
const matchedLine = detectSpawnStartupError(scrollback);
|
|
6673
6734
|
if (matchedLine !== void 0) {
|
|
6674
|
-
throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
|
|
6735
|
+
throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
|
|
6675
6736
|
}
|
|
6676
6737
|
}
|
|
6677
6738
|
}
|
|
@@ -6826,7 +6887,7 @@ var AgentNotInWorkstreamError = class extends Error {
|
|
|
6826
6887
|
}
|
|
6827
6888
|
};
|
|
6828
6889
|
var AgentDiedOnSpawnError = class extends Error {
|
|
6829
|
-
constructor(agentName, paneId, scrollback, command) {
|
|
6890
|
+
constructor(agentName, paneId, scrollback, command, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
|
|
6830
6891
|
const tail = scrollback?.trim();
|
|
6831
6892
|
const detail = tail ? `
|
|
6832
6893
|
|
|
@@ -6835,9 +6896,7 @@ ${tail}
|
|
|
6835
6896
|
--- end scrollback ---` : "";
|
|
6836
6897
|
const remote = /^\s*ssh\s/.test(command ?? "");
|
|
6837
6898
|
const cause = remote ? 'The command is an ssh hop, so the likely causes are remote: the remote binary is missing, or its env (PATH, provider keys) is absent because ssh runs a non-interactive shell that skips ~/.zshrc / ~/.bashrc (wrap it: $SHELL -ilc "pi --approve"); or sshd refused the socket forward (ExitOnForwardFailure=yes kills the connection; check AllowStreamLocalForwarding).' : "Most common cause: the spawned CLI exited immediately (e.g. a wrapper CLI blocking on its instance lock; set MU_<UPPER_CLI>_COMMAND to a non-blocking variant to bypass).";
|
|
6838
|
-
super(
|
|
6839
|
-
`agent ${agentName} died within ${defaultSpawnLivenessMs()}ms of spawn (pane ${paneId}). ${cause}${detail}`
|
|
6840
|
-
);
|
|
6899
|
+
super(`agent ${agentName} died ${when} (pane ${paneId}). ${cause}${detail}`);
|
|
6841
6900
|
this.agentName = agentName;
|
|
6842
6901
|
this.paneId = paneId;
|
|
6843
6902
|
this.scrollback = scrollback;
|
|
@@ -6896,9 +6955,9 @@ ${tail}
|
|
|
6896
6955
|
}
|
|
6897
6956
|
};
|
|
6898
6957
|
var AgentSpawnStartupError = class extends Error {
|
|
6899
|
-
constructor(agentName, paneId, matchedLine, scrollback) {
|
|
6958
|
+
constructor(agentName, paneId, matchedLine, scrollback, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
|
|
6900
6959
|
super(
|
|
6901
|
-
`agent ${agentName} reported a startup error
|
|
6960
|
+
`agent ${agentName} reported a startup error ${when} (pane ${paneId}). The pane is alive but the spawned CLI parked at an error prompt instead of becoming a working agent.
|
|
6902
6961
|
|
|
6903
6962
|
Matched line: ${matchedLine.trim()}
|
|
6904
6963
|
|
|
@@ -6984,7 +7043,14 @@ var AgentCtlUnreachableError = class extends Error {
|
|
|
6984
7043
|
kind;
|
|
6985
7044
|
name = "AgentCtlUnreachableError";
|
|
6986
7045
|
errorNextSteps() {
|
|
7046
|
+
const trust = this.kind === "missing" ? [
|
|
7047
|
+
{
|
|
7048
|
+
intent: "Just spawned? pi may sit at its project trust prompt: answer /trust in the pane, or respawn with --command 'pi --approve'",
|
|
7049
|
+
command: `mu agent read ${this.agentName} -w ${this.workstream}`
|
|
7050
|
+
}
|
|
7051
|
+
] : [];
|
|
6987
7052
|
return [
|
|
7053
|
+
...trust,
|
|
6988
7054
|
{ intent: "Check whether the mu pi extension is linked", command: "mu doctor" },
|
|
6989
7055
|
{
|
|
6990
7056
|
intent: "Stop a runaway tool without the socket",
|
|
@@ -7067,7 +7133,7 @@ var AgentFreshNeedsCtlError = class extends Error {
|
|
|
7067
7133
|
}
|
|
7068
7134
|
};
|
|
7069
7135
|
var AgentExtensionOutdatedError = class extends Error {
|
|
7070
|
-
constructor(agentName, workstream, op, extVersion) {
|
|
7136
|
+
constructor(agentName, workstream, op, extVersion, ops) {
|
|
7071
7137
|
super(
|
|
7072
7138
|
`agent ${agentName}'s mu pi extension (${extVersion ?? "version unknown"}) predates op ${op}: its pi loaded an older mu build; reload or respawn it`
|
|
7073
7139
|
);
|
|
@@ -7075,18 +7141,21 @@ var AgentExtensionOutdatedError = class extends Error {
|
|
|
7075
7141
|
this.workstream = workstream;
|
|
7076
7142
|
this.op = op;
|
|
7077
7143
|
this.extVersion = extVersion;
|
|
7144
|
+
this.ops = ops;
|
|
7078
7145
|
}
|
|
7079
7146
|
agentName;
|
|
7080
7147
|
workstream;
|
|
7081
7148
|
op;
|
|
7082
7149
|
extVersion;
|
|
7150
|
+
ops;
|
|
7083
7151
|
name = "AgentExtensionOutdatedError";
|
|
7084
7152
|
errorNextSteps() {
|
|
7085
7153
|
const w = `-w ${this.workstream}`;
|
|
7154
|
+
const via = this.ops?.includes("command") ? "" : " --via mux";
|
|
7086
7155
|
return [
|
|
7087
7156
|
{
|
|
7088
|
-
intent: "Reload pi's extensions
|
|
7089
|
-
command: `mu agent send ${this.agentName} '/reload'
|
|
7157
|
+
intent: "Reload pi's extensions (keeps the session and context)",
|
|
7158
|
+
command: `mu agent send ${this.agentName} '/reload'${via} ${w}`
|
|
7090
7159
|
},
|
|
7091
7160
|
{
|
|
7092
7161
|
intent: "Or respawn the agent (loses its context)",
|
|
@@ -7144,7 +7213,7 @@ var AgentSlashCommandUnsupportedError = class extends Error {
|
|
|
7144
7213
|
|
|
7145
7214
|
// src/agents/transport.ts
|
|
7146
7215
|
function agentCtlSocket(db, agent) {
|
|
7147
|
-
return ctlSocketPath(agent.workstreamName, agent.name,
|
|
7216
|
+
return ctlSocketPath(agent.workstreamName, agent.name, dirname7(db.name));
|
|
7148
7217
|
}
|
|
7149
7218
|
function expectsCtl(agent, sock = ctlSocketPath(agent.workstreamName, agent.name)) {
|
|
7150
7219
|
return speaksMuCtl(agent.cli, resolveCliCommand(agent.cli)) || existsSync8(sock);
|
|
@@ -7172,7 +7241,7 @@ async function extensionOutdated(agent, sock, e) {
|
|
|
7172
7241
|
if (hello.ok) extVersion = hello.extVersion;
|
|
7173
7242
|
} catch {
|
|
7174
7243
|
}
|
|
7175
|
-
return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion);
|
|
7244
|
+
return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion, e.ops);
|
|
7176
7245
|
}
|
|
7177
7246
|
async function ctlRequestFor(agent, sock, req) {
|
|
7178
7247
|
try {
|
|
@@ -7221,7 +7290,11 @@ async function sendViaTransport(agent, text, opts) {
|
|
|
7221
7290
|
}
|
|
7222
7291
|
throw new Error(`control socket refused the send: ${reply.error}`);
|
|
7223
7292
|
}
|
|
7224
|
-
return
|
|
7293
|
+
return {
|
|
7294
|
+
transport,
|
|
7295
|
+
...reply.state !== void 0 ? { state: reply.state } : {},
|
|
7296
|
+
...reply.runs !== void 0 ? { runs: reply.runs } : {}
|
|
7297
|
+
};
|
|
7225
7298
|
}
|
|
7226
7299
|
async function runSessionCommand(agent, sock, cmd, force) {
|
|
7227
7300
|
const reply = await ctlRequestFor(agent, sock, {
|
|
@@ -7422,7 +7495,9 @@ async function readAgentStates(agents, opts = {}) {
|
|
|
7422
7495
|
source: "ctl",
|
|
7423
7496
|
since: probe.status.since,
|
|
7424
7497
|
alive: true,
|
|
7425
|
-
ctl: "ok"
|
|
7498
|
+
ctl: "ok",
|
|
7499
|
+
pending: probe.status.pending,
|
|
7500
|
+
runs: probe.status.runs
|
|
7426
7501
|
});
|
|
7427
7502
|
} else {
|
|
7428
7503
|
failed.set(agentKey(agent), probe?.kind === "missing" ? "missing" : "refused");
|
|
@@ -8073,7 +8148,7 @@ function formatTreeNodeLabel(t, statusFn, opts = {}) {
|
|
|
8073
8148
|
|
|
8074
8149
|
// src/doctor-summary.ts
|
|
8075
8150
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
|
|
8076
|
-
import { delimiter as delimiter2, dirname as
|
|
8151
|
+
import { delimiter as delimiter2, dirname as dirname10, join as join11, parse } from "path";
|
|
8077
8152
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8078
8153
|
|
|
8079
8154
|
// src/drift.ts
|
|
@@ -8083,7 +8158,7 @@ import { join as join9 } from "path";
|
|
|
8083
8158
|
|
|
8084
8159
|
// src/rebuild.ts
|
|
8085
8160
|
import { existsSync as existsSync10, mkdirSync as mkdirSync3, statSync } from "fs";
|
|
8086
|
-
import { dirname as
|
|
8161
|
+
import { dirname as dirname8 } from "path";
|
|
8087
8162
|
var RebuildTargetExistsError = class extends Error {
|
|
8088
8163
|
constructor(path) {
|
|
8089
8164
|
super(`rebuild target already exists: ${path}`);
|
|
@@ -8146,7 +8221,7 @@ function rebuildInto(source, opts) {
|
|
|
8146
8221
|
if (existsSync10(targetPath) && opts.force !== true) {
|
|
8147
8222
|
throw new RebuildTargetExistsError(targetPath);
|
|
8148
8223
|
}
|
|
8149
|
-
mkdirSync3(
|
|
8224
|
+
mkdirSync3(dirname8(targetPath), { recursive: true });
|
|
8150
8225
|
const identity = source.prepare(
|
|
8151
8226
|
"SELECT machine_id, hostname, created_at, last_wall, last_counter FROM machine_identity WHERE id = 1"
|
|
8152
8227
|
).get();
|
|
@@ -8668,7 +8743,7 @@ import {
|
|
|
8668
8743
|
writeFileSync
|
|
8669
8744
|
} from "fs";
|
|
8670
8745
|
import { homedir as homedir4 } from "os";
|
|
8671
|
-
import { dirname as
|
|
8746
|
+
import { dirname as dirname9, join as join10, resolve as resolve5 } from "path";
|
|
8672
8747
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
8673
8748
|
var MU_SHIM_MARKER = "// mu:shim";
|
|
8674
8749
|
var LinkConflictError = class extends Error {
|
|
@@ -8698,7 +8773,7 @@ function resolveHome(opts) {
|
|
|
8698
8773
|
return opts?.home ?? process.env.MU_PI_HOME ?? homedir4();
|
|
8699
8774
|
}
|
|
8700
8775
|
function packageRoot() {
|
|
8701
|
-
return resolve5(
|
|
8776
|
+
return resolve5(dirname9(fileURLToPath(import.meta.url)), "..");
|
|
8702
8777
|
}
|
|
8703
8778
|
function extensionEntry() {
|
|
8704
8779
|
return process.env.MU_EXTENSION_ENTRY ?? join10(packageRoot(), "dist", "extension", "mu-pi.js");
|
|
@@ -8734,7 +8809,7 @@ function linkPi(opts) {
|
|
|
8734
8809
|
replacedCopy = !readFileSync(path, "utf8").includes(MU_SHIM_MARKER);
|
|
8735
8810
|
} catch {
|
|
8736
8811
|
}
|
|
8737
|
-
mkdirSync4(
|
|
8812
|
+
mkdirSync4(dirname9(path), { recursive: true });
|
|
8738
8813
|
writeFileSync(path, opts?.copy === true ? readFileSync(entry, "utf8") : shim(entry));
|
|
8739
8814
|
return { path, replacedCopy };
|
|
8740
8815
|
}
|
|
@@ -8745,7 +8820,7 @@ function linkSkill(opts) {
|
|
|
8745
8820
|
const st = lstatOrUndefined(path);
|
|
8746
8821
|
if (st) {
|
|
8747
8822
|
if (!st.isSymbolicLink()) throw new LinkConflictError(path, "not-a-symlink");
|
|
8748
|
-
const current = resolve5(
|
|
8823
|
+
const current = resolve5(dirname9(path), readlinkSync(path));
|
|
8749
8824
|
if (current === target) return { path, target };
|
|
8750
8825
|
if (existsSync12(current) && opts?.force !== true) {
|
|
8751
8826
|
throw new LinkConflictError(path, "foreign-symlink", current);
|
|
@@ -8753,7 +8828,7 @@ function linkSkill(opts) {
|
|
|
8753
8828
|
previous = current;
|
|
8754
8829
|
rmSync6(path);
|
|
8755
8830
|
}
|
|
8756
|
-
mkdirSync4(
|
|
8831
|
+
mkdirSync4(dirname9(path), { recursive: true });
|
|
8757
8832
|
symlinkSync(target, path, "dir");
|
|
8758
8833
|
return previous === void 0 ? { path, target } : { path, target, previous };
|
|
8759
8834
|
}
|
|
@@ -8787,7 +8862,7 @@ function inspectSkill(path) {
|
|
|
8787
8862
|
try {
|
|
8788
8863
|
target = realpathSync(path);
|
|
8789
8864
|
} catch {
|
|
8790
|
-
return { path, state: "dangling", target: resolve5(
|
|
8865
|
+
return { path, state: "dangling", target: resolve5(dirname9(path), readlinkSync(path)) };
|
|
8791
8866
|
}
|
|
8792
8867
|
let skillMd;
|
|
8793
8868
|
try {
|
|
@@ -8966,7 +9041,7 @@ function resolveExecutable(name) {
|
|
|
8966
9041
|
return null;
|
|
8967
9042
|
}
|
|
8968
9043
|
function murmurVersion(executable) {
|
|
8969
|
-
let dir =
|
|
9044
|
+
let dir = dirname10(executable);
|
|
8970
9045
|
const root = parse(dir).root;
|
|
8971
9046
|
while (dir !== root) {
|
|
8972
9047
|
const packagePath = join11(dir, "package.json");
|
|
@@ -8979,7 +9054,7 @@ function murmurVersion(executable) {
|
|
|
8979
9054
|
} catch {
|
|
8980
9055
|
}
|
|
8981
9056
|
}
|
|
8982
|
-
dir =
|
|
9057
|
+
dir = dirname10(dir);
|
|
8983
9058
|
}
|
|
8984
9059
|
return null;
|
|
8985
9060
|
}
|
|
@@ -9015,7 +9090,7 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
|
|
|
9015
9090
|
return {
|
|
9016
9091
|
name: "murmur",
|
|
9017
9092
|
status: "warn",
|
|
9018
|
-
detail: `murmur ${version} is older than 1.0.0; @murmur_pane_since missing, idle/stall timing unknown`
|
|
9093
|
+
detail: opts.muExtOk === true ? `murmur ${version} is older than 1.0.0; idle/stall timing unknown for non-pi CLIs (pi agents use ctl)` : `murmur ${version} is older than 1.0.0; @murmur_pane_since missing, idle/stall timing unknown`
|
|
9019
9094
|
};
|
|
9020
9095
|
}
|
|
9021
9096
|
return {
|
|
@@ -9124,11 +9199,14 @@ function remediationParagraph(check) {
|
|
|
9124
9199
|
return [
|
|
9125
9200
|
"mu talks to pi agents over a per-agent control socket served by",
|
|
9126
9201
|
"the mu pi extension. `missing`: no socket (extension not loaded,",
|
|
9127
|
-
"
|
|
9128
|
-
"
|
|
9202
|
+
"the agent predates `mu link pi`, or a new pi still waits at its",
|
|
9203
|
+
"project trust prompt: answer /trust in the pane, or spawn with",
|
|
9204
|
+
"--command 'pi --approve'). `refused`: socket present but not",
|
|
9205
|
+
"answering. `version`: protocol mismatch; upgrade mu or pi.",
|
|
9129
9206
|
"Run `mu link pi`, then respawn the agent. `extension X older than",
|
|
9130
|
-
"installed Y`: the agent's pi loaded an older mu
|
|
9131
|
-
"
|
|
9207
|
+
"installed Y` or `lacks ops`: the agent's pi loaded an older mu",
|
|
9208
|
+
"build; reload it with `mu agent send <a> '/reload'` (add",
|
|
9209
|
+
"`--via mux` when the extension lacks op command) or respawn it."
|
|
9132
9210
|
];
|
|
9133
9211
|
case "agents":
|
|
9134
9212
|
return [
|