@mu-crew/mu 3.6.2 → 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 +208 -117
- 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 +20 -4
- package/dist/index.js +130 -57
- 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 +5 -2
- 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];
|
|
@@ -20,6 +20,8 @@ interface StateReading {
|
|
|
20
20
|
/** ctl only: pi holds queued messages, so an idle reading is about to
|
|
21
21
|
* turn busy. */
|
|
22
22
|
pending?: boolean;
|
|
23
|
+
/** ctl only: pi's settled-run count (`mu agent wait --after-runs`). */
|
|
24
|
+
runs?: number;
|
|
23
25
|
}
|
|
24
26
|
interface StateAgentRef {
|
|
25
27
|
name: string;
|
|
@@ -1463,7 +1465,10 @@ type SpawnedAgent = AgentRow & {
|
|
|
1463
1465
|
* 4. setPaneTitle + enableMuPaneBordersForPane.
|
|
1464
1466
|
* 5. finalizeAgentRow() — patch placeholder pane_id to real (workspace
|
|
1465
1467
|
* path), or insert a fresh agent row (no-workspace path).
|
|
1466
|
-
* 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).
|
|
1467
1472
|
*
|
|
1468
1473
|
* Failure between any of (3)–(6) calls rollbackSpawn() to undo the
|
|
1469
1474
|
* pane + row + workspace. The caller-visible error is preserved.
|
|
@@ -1648,7 +1653,9 @@ declare class AgentDiedOnSpawnError extends Error implements HasNextSteps {
|
|
|
1648
1653
|
readonly remote: boolean;
|
|
1649
1654
|
constructor(agentName: string, paneId: string, scrollback: string | undefined,
|
|
1650
1655
|
/** The command the pane ran; an `ssh ...` command gets remote hints. */
|
|
1651
|
-
command?: string | undefined
|
|
1656
|
+
command?: string | undefined,
|
|
1657
|
+
/** When it died; default `within <MU_SPAWN_LIVENESS_MS>ms of spawn`. */
|
|
1658
|
+
when?: string);
|
|
1652
1659
|
errorNextSteps(): NextStep[];
|
|
1653
1660
|
}
|
|
1654
1661
|
/**
|
|
@@ -1694,7 +1701,9 @@ declare class AgentSpawnStartupError extends Error implements HasNextSteps {
|
|
|
1694
1701
|
matchedLine: string,
|
|
1695
1702
|
/** Full captured scrollback (tail-trimmed already by
|
|
1696
1703
|
* awaitSpawnLiveness). Attached to the message for context. */
|
|
1697
|
-
scrollback: string
|
|
1704
|
+
scrollback: string,
|
|
1705
|
+
/** When it was seen; default `within <MU_SPAWN_LIVENESS_MS>ms of spawn`. */
|
|
1706
|
+
when?: string);
|
|
1698
1707
|
errorNextSteps(): NextStep[];
|
|
1699
1708
|
}
|
|
1700
1709
|
/**
|
|
@@ -1921,6 +1930,13 @@ type Transport = "ctl" | "mux";
|
|
|
1921
1930
|
type SendResult = {
|
|
1922
1931
|
transport: Transport;
|
|
1923
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;
|
|
1924
1940
|
/** Set when the text ran as a pi session command over ctl. */
|
|
1925
1941
|
command?: CtlCommandName;
|
|
1926
1942
|
};
|
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
|
|
@@ -5601,12 +5637,15 @@ async function waitForTasks(db, input, opts) {
|
|
|
5601
5637
|
const ctlBrokenSince = /* @__PURE__ */ new Map();
|
|
5602
5638
|
const stuckLabel = /* @__PURE__ */ new Map();
|
|
5603
5639
|
const ctlOwners = /* @__PURE__ */ new Set();
|
|
5640
|
+
const ownerRuns = /* @__PURE__ */ new Map();
|
|
5604
5641
|
const stuckAgeMs = async (status, owner, workstreamName) => {
|
|
5605
5642
|
if (stuckAfterMs <= 0 || status !== "IN_PROGRESS" || owner === null || opts.readOwnerState === void 0)
|
|
5606
5643
|
return null;
|
|
5607
5644
|
const reading = await opts.readOwnerState({ name: owner, workstreamName });
|
|
5608
5645
|
const ownerKey = `${workstreamName}/${owner}`;
|
|
5609
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);
|
|
5610
5649
|
if (reading?.alive === true && (reading.ctl === "missing" || reading.ctl === "refused")) {
|
|
5611
5650
|
const firstSeen = ctlBrokenSince.get(ownerKey) ?? Date.now();
|
|
5612
5651
|
ctlBrokenSince.set(ownerKey, firstSeen);
|
|
@@ -5636,10 +5675,13 @@ async function waitForTasks(db, input, opts) {
|
|
|
5636
5675
|
stuckWarned.add(key);
|
|
5637
5676
|
const ageSecs = Math.round(ageMs / 1e3);
|
|
5638
5677
|
const ownerBit = owner ?? "<none>";
|
|
5678
|
+
const runs = ownerRuns.get(`${ref.workstreamName}/${ownerBit}`);
|
|
5639
5679
|
const stateBit = stuckLabel.get(`${ref.workstreamName}/${ownerBit}`) ?? "needs_input";
|
|
5680
|
+
const waitLine = stallWaitHint(ownerBit, ref.workstreamName, runs).map((s) => ` ${s.command}
|
|
5681
|
+
`).join("");
|
|
5640
5682
|
currentStuckWarn(
|
|
5641
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
|
|
5642
|
-
mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
|
|
5684
|
+
` + waitLine + ` mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
|
|
5643
5685
|
`
|
|
5644
5686
|
);
|
|
5645
5687
|
emitEvent(
|
|
@@ -5655,7 +5697,8 @@ async function waitForTasks(db, input, opts) {
|
|
|
5655
5697
|
owner,
|
|
5656
5698
|
ref.workstreamName,
|
|
5657
5699
|
ageSecs,
|
|
5658
|
-
ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`)
|
|
5700
|
+
ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`),
|
|
5701
|
+
runs
|
|
5659
5702
|
);
|
|
5660
5703
|
}
|
|
5661
5704
|
}
|
|
@@ -6089,13 +6132,6 @@ function deleteAgent(db, name, workstream) {
|
|
|
6089
6132
|
if (deleted) unlinkCtlSocket(db, name, workstream);
|
|
6090
6133
|
return deleted;
|
|
6091
6134
|
}
|
|
6092
|
-
function unlinkCtlSocket(db, name, workstream) {
|
|
6093
|
-
if (db.memory) return;
|
|
6094
|
-
try {
|
|
6095
|
-
rmSync3(ctlSocketPath(workstream, name, dirname4(db.name)), { force: true });
|
|
6096
|
-
} catch {
|
|
6097
|
-
}
|
|
6098
|
-
}
|
|
6099
6135
|
function deleteAgentRow(db, name, workstream) {
|
|
6100
6136
|
return withOpContext(
|
|
6101
6137
|
db,
|
|
@@ -6191,7 +6227,7 @@ async function listLiveAgents(db, opts) {
|
|
|
6191
6227
|
...opts.mode !== void 0 ? { mode: opts.mode } : {}
|
|
6192
6228
|
});
|
|
6193
6229
|
const baseAgents = listAgents(db, { workstream: opts.workstream });
|
|
6194
|
-
const readings = await readAgentStates(baseAgents, { stateDir:
|
|
6230
|
+
const readings = await readAgentStates(baseAgents, { stateDir: dirname5(db.name) });
|
|
6195
6231
|
const now = Date.now();
|
|
6196
6232
|
const agents = baseAgents.map((agent) => {
|
|
6197
6233
|
const reading = readings.get(agentKey(agent)) ?? {
|
|
@@ -6456,7 +6492,7 @@ function defaultSpawnCtlMs() {
|
|
|
6456
6492
|
return parsed;
|
|
6457
6493
|
}
|
|
6458
6494
|
var CTL_POLL_INTERVAL_MS = 250;
|
|
6459
|
-
async function awaitCtlHandshake(sock) {
|
|
6495
|
+
async function awaitCtlHandshake(sock, onMiss) {
|
|
6460
6496
|
const budgetMs = defaultSpawnCtlMs();
|
|
6461
6497
|
const deadline = Date.now() + budgetMs;
|
|
6462
6498
|
for (; ; ) {
|
|
@@ -6464,6 +6500,7 @@ async function awaitCtlHandshake(sock) {
|
|
|
6464
6500
|
const probe = await ctlProbe(sock, Math.max(1, Math.min(remaining, CTL_POLL_INTERVAL_MS * 4)));
|
|
6465
6501
|
if (probe.kind === "ok") return "ok";
|
|
6466
6502
|
const last = probeToSpawnCtl(probe.kind);
|
|
6503
|
+
if (onMiss) await onMiss();
|
|
6467
6504
|
const left = deadline - Date.now();
|
|
6468
6505
|
if (left <= 0) return last;
|
|
6469
6506
|
await delay(Math.min(CTL_POLL_INTERVAL_MS, left));
|
|
@@ -6491,8 +6528,8 @@ async function spawnAgent(db, opts) {
|
|
|
6491
6528
|
}
|
|
6492
6529
|
}
|
|
6493
6530
|
const workspacePathStr = opts.workspace ? await prestageWorkspace(db, opts, cli) : void 0;
|
|
6494
|
-
const ctlSocket = ctlSocketPath(opts.workstream, opts.name,
|
|
6495
|
-
mkdirSync2(
|
|
6531
|
+
const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname6(db.name));
|
|
6532
|
+
mkdirSync2(dirname6(ctlSocket), { recursive: true, mode: 448 });
|
|
6496
6533
|
rmSync4(ctlSocket, { force: true });
|
|
6497
6534
|
const paneEnv = {
|
|
6498
6535
|
MU_MANAGED_AGENT: "1",
|
|
@@ -6503,6 +6540,8 @@ async function spawnAgent(db, opts) {
|
|
|
6503
6540
|
const hasWorkspace = workspacePathStr !== void 0;
|
|
6504
6541
|
let paneId;
|
|
6505
6542
|
let agent;
|
|
6543
|
+
const wantsCtl = opts.ctl !== false && speaksMuCtl(cli, command) && defaultSpawnCtlMs() !== 0;
|
|
6544
|
+
let ctl;
|
|
6506
6545
|
try {
|
|
6507
6546
|
const created = await withSpawnLock(session, async () => {
|
|
6508
6547
|
const pid = await createOrReusePane({
|
|
@@ -6523,7 +6562,23 @@ async function spawnAgent(db, opts) {
|
|
|
6523
6562
|
agent = created.row;
|
|
6524
6563
|
const startAgent = (await activeMux()).startAgentInPane;
|
|
6525
6564
|
if (startAgent === void 0) {
|
|
6526
|
-
|
|
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
|
+
}
|
|
6527
6582
|
} else {
|
|
6528
6583
|
await startAgent({ paneId, name: opts.name, cli, command, commandSource });
|
|
6529
6584
|
}
|
|
@@ -6532,14 +6587,13 @@ async function spawnAgent(db, opts) {
|
|
|
6532
6587
|
if (hasWorkspace) attachOrphanCleanupHint(err, opts.name, opts.workstream);
|
|
6533
6588
|
throw err;
|
|
6534
6589
|
}
|
|
6535
|
-
|
|
6590
|
+
ctl ??= wantsCtl ? await awaitCtlHandshake(ctlSocket) : "skipped";
|
|
6536
6591
|
emitEvent(
|
|
6537
6592
|
db,
|
|
6538
6593
|
opts.workstream,
|
|
6539
6594
|
"agent.spawn",
|
|
6540
6595
|
`agent spawn ${opts.name} (cli=${cli}, role=${opts.role ?? "full-access"}, pane=${paneId})`
|
|
6541
6596
|
);
|
|
6542
|
-
await refreshAgentTitle(db, opts.name, opts.workstream);
|
|
6543
6597
|
maybeWarnNonConventionalAgentName(opts.name);
|
|
6544
6598
|
return { ...agent, ctl, ctlSocket };
|
|
6545
6599
|
}
|
|
@@ -6667,15 +6721,18 @@ async function awaitSpawnLiveness(paneId, agentName, command) {
|
|
|
6667
6721
|
const ms = defaultSpawnLivenessMs();
|
|
6668
6722
|
if (ms === 0) return;
|
|
6669
6723
|
await sleep(ms);
|
|
6724
|
+
await checkSpawnHealth(paneId, agentName, command);
|
|
6725
|
+
}
|
|
6726
|
+
async function checkSpawnHealth(paneId, agentName, command, when) {
|
|
6670
6727
|
const mux = await activeMux();
|
|
6671
6728
|
const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
|
|
6672
6729
|
if (!await mux.paneExists(paneId)) {
|
|
6673
|
-
throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command);
|
|
6730
|
+
throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when);
|
|
6674
6731
|
}
|
|
6675
6732
|
if (scrollback !== void 0) {
|
|
6676
6733
|
const matchedLine = detectSpawnStartupError(scrollback);
|
|
6677
6734
|
if (matchedLine !== void 0) {
|
|
6678
|
-
throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
|
|
6735
|
+
throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
|
|
6679
6736
|
}
|
|
6680
6737
|
}
|
|
6681
6738
|
}
|
|
@@ -6830,7 +6887,7 @@ var AgentNotInWorkstreamError = class extends Error {
|
|
|
6830
6887
|
}
|
|
6831
6888
|
};
|
|
6832
6889
|
var AgentDiedOnSpawnError = class extends Error {
|
|
6833
|
-
constructor(agentName, paneId, scrollback, command) {
|
|
6890
|
+
constructor(agentName, paneId, scrollback, command, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
|
|
6834
6891
|
const tail = scrollback?.trim();
|
|
6835
6892
|
const detail = tail ? `
|
|
6836
6893
|
|
|
@@ -6839,9 +6896,7 @@ ${tail}
|
|
|
6839
6896
|
--- end scrollback ---` : "";
|
|
6840
6897
|
const remote = /^\s*ssh\s/.test(command ?? "");
|
|
6841
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).";
|
|
6842
|
-
super(
|
|
6843
|
-
`agent ${agentName} died within ${defaultSpawnLivenessMs()}ms of spawn (pane ${paneId}). ${cause}${detail}`
|
|
6844
|
-
);
|
|
6899
|
+
super(`agent ${agentName} died ${when} (pane ${paneId}). ${cause}${detail}`);
|
|
6845
6900
|
this.agentName = agentName;
|
|
6846
6901
|
this.paneId = paneId;
|
|
6847
6902
|
this.scrollback = scrollback;
|
|
@@ -6900,9 +6955,9 @@ ${tail}
|
|
|
6900
6955
|
}
|
|
6901
6956
|
};
|
|
6902
6957
|
var AgentSpawnStartupError = class extends Error {
|
|
6903
|
-
constructor(agentName, paneId, matchedLine, scrollback) {
|
|
6958
|
+
constructor(agentName, paneId, matchedLine, scrollback, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
|
|
6904
6959
|
super(
|
|
6905
|
-
`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.
|
|
6906
6961
|
|
|
6907
6962
|
Matched line: ${matchedLine.trim()}
|
|
6908
6963
|
|
|
@@ -6988,7 +7043,14 @@ var AgentCtlUnreachableError = class extends Error {
|
|
|
6988
7043
|
kind;
|
|
6989
7044
|
name = "AgentCtlUnreachableError";
|
|
6990
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
|
+
] : [];
|
|
6991
7052
|
return [
|
|
7053
|
+
...trust,
|
|
6992
7054
|
{ intent: "Check whether the mu pi extension is linked", command: "mu doctor" },
|
|
6993
7055
|
{
|
|
6994
7056
|
intent: "Stop a runaway tool without the socket",
|
|
@@ -7071,7 +7133,7 @@ var AgentFreshNeedsCtlError = class extends Error {
|
|
|
7071
7133
|
}
|
|
7072
7134
|
};
|
|
7073
7135
|
var AgentExtensionOutdatedError = class extends Error {
|
|
7074
|
-
constructor(agentName, workstream, op, extVersion) {
|
|
7136
|
+
constructor(agentName, workstream, op, extVersion, ops) {
|
|
7075
7137
|
super(
|
|
7076
7138
|
`agent ${agentName}'s mu pi extension (${extVersion ?? "version unknown"}) predates op ${op}: its pi loaded an older mu build; reload or respawn it`
|
|
7077
7139
|
);
|
|
@@ -7079,18 +7141,21 @@ var AgentExtensionOutdatedError = class extends Error {
|
|
|
7079
7141
|
this.workstream = workstream;
|
|
7080
7142
|
this.op = op;
|
|
7081
7143
|
this.extVersion = extVersion;
|
|
7144
|
+
this.ops = ops;
|
|
7082
7145
|
}
|
|
7083
7146
|
agentName;
|
|
7084
7147
|
workstream;
|
|
7085
7148
|
op;
|
|
7086
7149
|
extVersion;
|
|
7150
|
+
ops;
|
|
7087
7151
|
name = "AgentExtensionOutdatedError";
|
|
7088
7152
|
errorNextSteps() {
|
|
7089
7153
|
const w = `-w ${this.workstream}`;
|
|
7154
|
+
const via = this.ops?.includes("command") ? "" : " --via mux";
|
|
7090
7155
|
return [
|
|
7091
7156
|
{
|
|
7092
|
-
intent: "Reload pi's extensions
|
|
7093
|
-
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}`
|
|
7094
7159
|
},
|
|
7095
7160
|
{
|
|
7096
7161
|
intent: "Or respawn the agent (loses its context)",
|
|
@@ -7148,7 +7213,7 @@ var AgentSlashCommandUnsupportedError = class extends Error {
|
|
|
7148
7213
|
|
|
7149
7214
|
// src/agents/transport.ts
|
|
7150
7215
|
function agentCtlSocket(db, agent) {
|
|
7151
|
-
return ctlSocketPath(agent.workstreamName, agent.name,
|
|
7216
|
+
return ctlSocketPath(agent.workstreamName, agent.name, dirname7(db.name));
|
|
7152
7217
|
}
|
|
7153
7218
|
function expectsCtl(agent, sock = ctlSocketPath(agent.workstreamName, agent.name)) {
|
|
7154
7219
|
return speaksMuCtl(agent.cli, resolveCliCommand(agent.cli)) || existsSync8(sock);
|
|
@@ -7176,7 +7241,7 @@ async function extensionOutdated(agent, sock, e) {
|
|
|
7176
7241
|
if (hello.ok) extVersion = hello.extVersion;
|
|
7177
7242
|
} catch {
|
|
7178
7243
|
}
|
|
7179
|
-
return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion);
|
|
7244
|
+
return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion, e.ops);
|
|
7180
7245
|
}
|
|
7181
7246
|
async function ctlRequestFor(agent, sock, req) {
|
|
7182
7247
|
try {
|
|
@@ -7225,7 +7290,11 @@ async function sendViaTransport(agent, text, opts) {
|
|
|
7225
7290
|
}
|
|
7226
7291
|
throw new Error(`control socket refused the send: ${reply.error}`);
|
|
7227
7292
|
}
|
|
7228
|
-
return
|
|
7293
|
+
return {
|
|
7294
|
+
transport,
|
|
7295
|
+
...reply.state !== void 0 ? { state: reply.state } : {},
|
|
7296
|
+
...reply.runs !== void 0 ? { runs: reply.runs } : {}
|
|
7297
|
+
};
|
|
7229
7298
|
}
|
|
7230
7299
|
async function runSessionCommand(agent, sock, cmd, force) {
|
|
7231
7300
|
const reply = await ctlRequestFor(agent, sock, {
|
|
@@ -7427,7 +7496,8 @@ async function readAgentStates(agents, opts = {}) {
|
|
|
7427
7496
|
since: probe.status.since,
|
|
7428
7497
|
alive: true,
|
|
7429
7498
|
ctl: "ok",
|
|
7430
|
-
pending: probe.status.pending
|
|
7499
|
+
pending: probe.status.pending,
|
|
7500
|
+
runs: probe.status.runs
|
|
7431
7501
|
});
|
|
7432
7502
|
} else {
|
|
7433
7503
|
failed.set(agentKey(agent), probe?.kind === "missing" ? "missing" : "refused");
|
|
@@ -8078,7 +8148,7 @@ function formatTreeNodeLabel(t, statusFn, opts = {}) {
|
|
|
8078
8148
|
|
|
8079
8149
|
// src/doctor-summary.ts
|
|
8080
8150
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
|
|
8081
|
-
import { delimiter as delimiter2, dirname as
|
|
8151
|
+
import { delimiter as delimiter2, dirname as dirname10, join as join11, parse } from "path";
|
|
8082
8152
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8083
8153
|
|
|
8084
8154
|
// src/drift.ts
|
|
@@ -8088,7 +8158,7 @@ import { join as join9 } from "path";
|
|
|
8088
8158
|
|
|
8089
8159
|
// src/rebuild.ts
|
|
8090
8160
|
import { existsSync as existsSync10, mkdirSync as mkdirSync3, statSync } from "fs";
|
|
8091
|
-
import { dirname as
|
|
8161
|
+
import { dirname as dirname8 } from "path";
|
|
8092
8162
|
var RebuildTargetExistsError = class extends Error {
|
|
8093
8163
|
constructor(path) {
|
|
8094
8164
|
super(`rebuild target already exists: ${path}`);
|
|
@@ -8151,7 +8221,7 @@ function rebuildInto(source, opts) {
|
|
|
8151
8221
|
if (existsSync10(targetPath) && opts.force !== true) {
|
|
8152
8222
|
throw new RebuildTargetExistsError(targetPath);
|
|
8153
8223
|
}
|
|
8154
|
-
mkdirSync3(
|
|
8224
|
+
mkdirSync3(dirname8(targetPath), { recursive: true });
|
|
8155
8225
|
const identity = source.prepare(
|
|
8156
8226
|
"SELECT machine_id, hostname, created_at, last_wall, last_counter FROM machine_identity WHERE id = 1"
|
|
8157
8227
|
).get();
|
|
@@ -8673,7 +8743,7 @@ import {
|
|
|
8673
8743
|
writeFileSync
|
|
8674
8744
|
} from "fs";
|
|
8675
8745
|
import { homedir as homedir4 } from "os";
|
|
8676
|
-
import { dirname as
|
|
8746
|
+
import { dirname as dirname9, join as join10, resolve as resolve5 } from "path";
|
|
8677
8747
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
8678
8748
|
var MU_SHIM_MARKER = "// mu:shim";
|
|
8679
8749
|
var LinkConflictError = class extends Error {
|
|
@@ -8703,7 +8773,7 @@ function resolveHome(opts) {
|
|
|
8703
8773
|
return opts?.home ?? process.env.MU_PI_HOME ?? homedir4();
|
|
8704
8774
|
}
|
|
8705
8775
|
function packageRoot() {
|
|
8706
|
-
return resolve5(
|
|
8776
|
+
return resolve5(dirname9(fileURLToPath(import.meta.url)), "..");
|
|
8707
8777
|
}
|
|
8708
8778
|
function extensionEntry() {
|
|
8709
8779
|
return process.env.MU_EXTENSION_ENTRY ?? join10(packageRoot(), "dist", "extension", "mu-pi.js");
|
|
@@ -8739,7 +8809,7 @@ function linkPi(opts) {
|
|
|
8739
8809
|
replacedCopy = !readFileSync(path, "utf8").includes(MU_SHIM_MARKER);
|
|
8740
8810
|
} catch {
|
|
8741
8811
|
}
|
|
8742
|
-
mkdirSync4(
|
|
8812
|
+
mkdirSync4(dirname9(path), { recursive: true });
|
|
8743
8813
|
writeFileSync(path, opts?.copy === true ? readFileSync(entry, "utf8") : shim(entry));
|
|
8744
8814
|
return { path, replacedCopy };
|
|
8745
8815
|
}
|
|
@@ -8750,7 +8820,7 @@ function linkSkill(opts) {
|
|
|
8750
8820
|
const st = lstatOrUndefined(path);
|
|
8751
8821
|
if (st) {
|
|
8752
8822
|
if (!st.isSymbolicLink()) throw new LinkConflictError(path, "not-a-symlink");
|
|
8753
|
-
const current = resolve5(
|
|
8823
|
+
const current = resolve5(dirname9(path), readlinkSync(path));
|
|
8754
8824
|
if (current === target) return { path, target };
|
|
8755
8825
|
if (existsSync12(current) && opts?.force !== true) {
|
|
8756
8826
|
throw new LinkConflictError(path, "foreign-symlink", current);
|
|
@@ -8758,7 +8828,7 @@ function linkSkill(opts) {
|
|
|
8758
8828
|
previous = current;
|
|
8759
8829
|
rmSync6(path);
|
|
8760
8830
|
}
|
|
8761
|
-
mkdirSync4(
|
|
8831
|
+
mkdirSync4(dirname9(path), { recursive: true });
|
|
8762
8832
|
symlinkSync(target, path, "dir");
|
|
8763
8833
|
return previous === void 0 ? { path, target } : { path, target, previous };
|
|
8764
8834
|
}
|
|
@@ -8792,7 +8862,7 @@ function inspectSkill(path) {
|
|
|
8792
8862
|
try {
|
|
8793
8863
|
target = realpathSync(path);
|
|
8794
8864
|
} catch {
|
|
8795
|
-
return { path, state: "dangling", target: resolve5(
|
|
8865
|
+
return { path, state: "dangling", target: resolve5(dirname9(path), readlinkSync(path)) };
|
|
8796
8866
|
}
|
|
8797
8867
|
let skillMd;
|
|
8798
8868
|
try {
|
|
@@ -8971,7 +9041,7 @@ function resolveExecutable(name) {
|
|
|
8971
9041
|
return null;
|
|
8972
9042
|
}
|
|
8973
9043
|
function murmurVersion(executable) {
|
|
8974
|
-
let dir =
|
|
9044
|
+
let dir = dirname10(executable);
|
|
8975
9045
|
const root = parse(dir).root;
|
|
8976
9046
|
while (dir !== root) {
|
|
8977
9047
|
const packagePath = join11(dir, "package.json");
|
|
@@ -8984,7 +9054,7 @@ function murmurVersion(executable) {
|
|
|
8984
9054
|
} catch {
|
|
8985
9055
|
}
|
|
8986
9056
|
}
|
|
8987
|
-
dir =
|
|
9057
|
+
dir = dirname10(dir);
|
|
8988
9058
|
}
|
|
8989
9059
|
return null;
|
|
8990
9060
|
}
|
|
@@ -9020,7 +9090,7 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
|
|
|
9020
9090
|
return {
|
|
9021
9091
|
name: "murmur",
|
|
9022
9092
|
status: "warn",
|
|
9023
|
-
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`
|
|
9024
9094
|
};
|
|
9025
9095
|
}
|
|
9026
9096
|
return {
|
|
@@ -9129,11 +9199,14 @@ function remediationParagraph(check) {
|
|
|
9129
9199
|
return [
|
|
9130
9200
|
"mu talks to pi agents over a per-agent control socket served by",
|
|
9131
9201
|
"the mu pi extension. `missing`: no socket (extension not loaded,",
|
|
9132
|
-
"
|
|
9133
|
-
"
|
|
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.",
|
|
9134
9206
|
"Run `mu link pi`, then respawn the agent. `extension X older than",
|
|
9135
|
-
"installed Y`: the agent's pi loaded an older mu
|
|
9136
|
-
"
|
|
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."
|
|
9137
9210
|
];
|
|
9138
9211
|
case "agents":
|
|
9139
9212
|
return [
|