@mu-crew/mu 3.9.0 → 3.10.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.
@@ -182,7 +182,6 @@ function registerDelegate(pi, run = defaultRunner()) {
182
182
  const queue = [];
183
183
  let queueSeq = 0;
184
184
  const failed = /* @__PURE__ */ new Set();
185
- const reserved = /* @__PURE__ */ new Set();
186
185
  let ui;
187
186
  let lastPending = 0;
188
187
  const reportPending = () => {
@@ -209,7 +208,6 @@ function registerDelegate(pi, run = defaultRunner()) {
209
208
  };
210
209
  const forget = (name) => {
211
210
  inflight.delete(name);
212
- reserved.delete(name);
213
211
  drain();
214
212
  showStatus();
215
213
  };
@@ -248,63 +246,57 @@ function registerDelegate(pi, run = defaultRunner()) {
248
246
  }
249
247
  };
250
248
  const failure = (what, r) => new Error(`${what} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
251
- async function pickName(stem) {
252
- const r = await mu(["agent", "list", "-w", W, "--json"]);
253
- const agents = json(r)?.agents;
254
- const taken = new Set(
255
- Array.isArray(agents) ? agents.map((a) => a.name).filter((n) => n) : []
256
- );
257
- if (Array.isArray(agents)) {
249
+ function pruneFailed() {
250
+ if (failed.size === 0) return;
251
+ void mu(["agent", "list", "-w", W, "--json"]).then((r) => {
252
+ const agents = json(r)?.agents;
253
+ if (!Array.isArray(agents)) return;
254
+ const taken = new Set(agents.map((a) => a.name));
258
255
  for (const f of [...failed]) if (!taken.has(f)) failed.delete(f);
259
256
  showStatus();
260
- }
261
- for (let n = 1; ; n++) {
262
- const name = stem ? n === 1 ? `delegate-${stem}` : `delegate-${stem}-${n}` : `delegate-${n}`;
263
- if (!taken.has(name) && !reserved.has(name)) {
264
- reserved.add(name);
265
- return name;
266
- }
267
- }
257
+ });
268
258
  }
269
- async function spawn(params, cwd, signal) {
259
+ async function spawn(params, text, cwd, signal) {
260
+ signal?.throwIfAborted();
261
+ pruneFailed();
270
262
  const stem = labelStem(params.label);
271
- for (let attempt = 0; ; attempt++) {
272
- signal?.throwIfAborted();
273
- const name = await pickName(stem);
274
- const args = ["agent", "spawn", name, "-w", W, "--json"];
275
- if (params.workspace === true) args.push("--workspace");
276
- else if (cwd) args.push("--cwd", cwd);
277
- if (typeof params.cli === "string" && params.cli) args.push("--cli", params.cli);
278
- const r = await mu(args);
279
- if (signal?.aborted && r.code === 0) {
280
- reserved.delete(name);
281
- await mu(["agent", "close", name, "-w", W]);
282
- signal.throwIfAborted();
283
- }
284
- if (r.code !== 0 && /already exists/.test(r.stderr) && attempt < 3) {
285
- reserved.delete(name);
286
- continue;
287
- }
288
- if (r.code !== 0) {
289
- reserved.delete(name);
290
- throw failure("mu agent spawn", r);
291
- }
292
- const out = json(r) ?? {};
293
- if (out.ctl !== "ok") {
294
- reserved.delete(name);
295
- throw new Error(
296
- `${W}/${name} spawned but its control socket is ${String(out.ctl)}, so its answer cannot come back. Pane kept; close it with: mu agent close ${name} -w ${W}`
297
- );
298
- }
299
- const steps = Array.isArray(out.nextSteps) ? out.nextSteps : [];
300
- const attach = steps.map((s) => s).find((s) => s.intent === "Attach the pane")?.command;
301
- const wsPath = out.workspace?.path;
302
- return {
303
- name,
304
- ...typeof attach === "string" ? { attach } : {},
305
- ...typeof wsPath === "string" ? { workspace: wsPath } : {}
306
- };
263
+ const base = stem ? `delegate-${stem}` : "delegate-1";
264
+ const args = ["agent", "spawn", base, "-w", W, "--next-free", "--send", text, "--json"];
265
+ if (params.workspace === true) args.push("--workspace");
266
+ else if (cwd) args.push("--cwd", cwd);
267
+ if (typeof params.cli === "string" && params.cli) args.push("--cli", params.cli);
268
+ const r = await mu(args);
269
+ const out = json(r);
270
+ const spawned = out?.agent?.name;
271
+ if (typeof spawned !== "string") throw failure("mu agent spawn", r);
272
+ const name = spawned;
273
+ if (signal?.aborted) {
274
+ await mu(["agent", "close", name, "-w", W]);
275
+ signal.throwIfAborted();
276
+ }
277
+ if (out?.ctl !== "ok") {
278
+ throw new Error(
279
+ `${W}/${name} spawned but its control socket is ${String(out?.ctl)}, so its answer cannot come back. Pane kept; close it with: mu agent close ${name} -w ${W}`
280
+ );
307
281
  }
282
+ const sent = out.send;
283
+ if (sent?.ok !== true) {
284
+ const c = await mu(["agent", "close", name, "-w", W]);
285
+ const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
286
+ const why = typeof sent?.message === "string" ? sent.message : (r.stderr || r.stdout).trim();
287
+ throw new Error(`mu agent send to ${W}/${name} failed (exit ${r.code}): ${why}. ${pane}`);
288
+ }
289
+ const steps = Array.isArray(out.nextSteps) ? out.nextSteps : [];
290
+ const attach = steps.map((s) => s).find((s) => s.intent === "Attach the pane")?.command;
291
+ const wsPath = out.workspace?.path;
292
+ return {
293
+ name,
294
+ // A pi just spawned has settled no run, so 0 is exact when an older
295
+ // extension's reply carries no runs.
296
+ runs: typeof sent.runs === "number" ? sent.runs : 0,
297
+ ...typeof attach === "string" ? { attach } : {},
298
+ ...typeof wsPath === "string" ? { workspace: wsPath } : {}
299
+ };
308
300
  }
309
301
  const recordLabel = (params, name) => typeof params.label === "string" && params.label.trim() ? params.label.trim() : name;
310
302
  async function resolveRecord(raw) {
@@ -400,7 +392,14 @@ ${recorded}` : message,
400
392
  cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
401
393
  if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
402
394
  }
403
- const { name, attach, workspace } = await spawn(params, cwd, signal);
395
+ const { name, runs, attach, workspace } = await spawn(
396
+ params,
397
+ brief ? `${brief}
398
+
399
+ ${task}` : task,
400
+ cwd,
401
+ signal
402
+ );
404
403
  const entry = {
405
404
  abort: new AbortController(),
406
405
  cancelling: false,
@@ -410,43 +409,21 @@ ${recorded}` : message,
410
409
  freeSlot();
411
410
  showStatus();
412
411
  const started = Date.now();
413
- const waitArgs = (after) => [
414
- "agent",
415
- "wait",
416
- name,
417
- "-w",
418
- W,
419
- "--json",
420
- "--timeout",
421
- String(timeoutS),
422
- ...after !== void 0 ? ["--after-runs", String(after)] : []
423
- ];
424
- const plain = new AbortController();
425
- entry.abort.signal.addEventListener("abort", () => plain.abort(), { once: true });
426
- let waiting = mu(waitArgs(), plain.signal);
427
- const sent = await mu([
428
- "agent",
429
- "send",
430
- name,
431
- brief ? `${brief}
432
-
433
- ${task}` : task,
434
- "-w",
435
- W,
436
- "--json"
437
- ]);
438
- if (sent.code !== 0) {
439
- plain.abort();
440
- forget(name);
441
- const c = await mu(["agent", "close", name, "-w", W]);
442
- const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
443
- throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
444
- }
445
- const runs = json(sent)?.runs;
446
- if (typeof runs === "number") {
447
- plain.abort();
448
- waiting = mu(waitArgs(runs), entry.abort.signal);
449
- }
412
+ const waiting = mu(
413
+ [
414
+ "agent",
415
+ "wait",
416
+ name,
417
+ "-w",
418
+ W,
419
+ "--json",
420
+ "--timeout",
421
+ String(timeoutS),
422
+ "--after-runs",
423
+ String(runs)
424
+ ],
425
+ entry.abort.signal
426
+ );
450
427
  const settle = async (r) => {
451
428
  forget(name);
452
429
  try {
@@ -892,6 +869,54 @@ function nudgeText(rule, refs) {
892
869
  `${refs.length} task(s) IN_PROGRESS. Next: mu task wait ${shown} --first --on-stall exit --json${more}`
893
870
  ].join("\n");
894
871
  }
872
+ function breadcrumb(run, args) {
873
+ void run(args).catch(() => {
874
+ });
875
+ }
876
+ function parallelSettle(pi) {
877
+ const handlers = [];
878
+ const api = {
879
+ on(event, handler) {
880
+ if (event === "agent_before_settle") handlers.push(handler);
881
+ else pi.on(event, handler);
882
+ }
883
+ };
884
+ let errors = [];
885
+ return {
886
+ api,
887
+ flush() {
888
+ if (handlers.length === 0) return;
889
+ pi.on("agent_before_settle", async (event, ctx) => {
890
+ errors = handlers.map(() => void 0);
891
+ const results = await Promise.all(
892
+ handlers.map(async (h, i) => {
893
+ try {
894
+ return await h(event, ctx);
895
+ } catch (err) {
896
+ errors[i] = [err];
897
+ return void 0;
898
+ }
899
+ })
900
+ );
901
+ const fired = results.filter((r) => r !== void 0);
902
+ if (fired.length === 0) return void 0;
903
+ const prior = event?.entries ?? [];
904
+ return {
905
+ entries: [...prior, ...fired.flatMap((r) => r.entries ?? [])],
906
+ ...fired.some((r) => r.continue === true) ? { continue: true } : {}
907
+ };
908
+ });
909
+ handlers.forEach((_, i) => {
910
+ pi.on("agent_before_settle", () => {
911
+ const err = errors[i];
912
+ errors[i] = void 0;
913
+ if (err !== void 0) throw err[0];
914
+ return void 0;
915
+ });
916
+ });
917
+ }
918
+ };
919
+ }
895
920
  function outcomeOf(event) {
896
921
  return typeof event === "object" && event !== null ? event.outcome : void 0;
897
922
  }
@@ -923,7 +948,7 @@ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill(), nested
923
948
  if (refs === void 0 || refs.length === 0) return;
924
949
  const text = nudgeText(rule, refs);
925
950
  const logWs = [...armed].find((w) => w !== "");
926
- await run([
951
+ breadcrumb(run, [
927
952
  "log",
928
953
  ...logWs ? ["-w", logWs] : [],
929
954
  "--kind",
@@ -979,7 +1004,7 @@ function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested
979
1004
  fired = true;
980
1005
  const names = await ownedInProgress(run, me.agent, me.workstream);
981
1006
  if (names === void 0 || names.length === 0) return;
982
- await run([
1007
+ breadcrumb(run, [
983
1008
  "log",
984
1009
  "-w",
985
1010
  me.workstream,
@@ -1033,13 +1058,12 @@ function registerRefuteNudge(pi, run = defaultRunner(), nested = notNested) {
1033
1058
  if (fired || armed.size === 0 || nested() || noUI(ctx)) return;
1034
1059
  if (outcomeOf(event) !== "completed") return;
1035
1060
  fired = true;
1036
- const refs = [];
1037
- for (const [ref, t] of armed) {
1038
- if (await briefRefuted(run, t.ws, t.id) === false) refs.push(ref);
1039
- }
1061
+ const tasks = [...armed];
1062
+ const refuted = await Promise.all(tasks.map(([, t]) => briefRefuted(run, t.ws, t.id)));
1063
+ const refs = tasks.flatMap(([ref], i) => refuted[i] === false ? [ref] : []);
1040
1064
  if (refs.length === 0) return;
1041
1065
  const logWs = [...armed.values()].find((t) => t.ws !== "")?.ws;
1042
- await run([
1066
+ breadcrumb(run, [
1043
1067
  "log",
1044
1068
  ...logWs ? ["-w", logWs] : [],
1045
1069
  "--kind",
@@ -1060,7 +1084,7 @@ function registerRefuteNudge(pi, run = defaultRunner(), nested = notNested) {
1060
1084
  }
1061
1085
 
1062
1086
  // extension/mu-pi.ts
1063
- var EXT_VERSION = true ? "3.9.0" : void 0;
1087
+ var EXT_VERSION = true ? "3.10.0" : void 0;
1064
1088
  var FRESH_COMMAND = "mu-fresh";
1065
1089
  var FRESH_TIMEOUT_MS = 3e4;
1066
1090
  function commandName(name) {
@@ -1203,9 +1227,11 @@ function muPi(pi, run = defaultRunner()) {
1203
1227
  registerDelegate(pi, run);
1204
1228
  registerPrompts(pi);
1205
1229
  const nested = serveCtl(pi);
1206
- registerNudge(pi, run, void 0, nested);
1207
- registerCloseNudge(pi, run, process.env, nested);
1208
- registerRefuteNudge(pi, run, nested);
1230
+ const settle = parallelSettle(pi);
1231
+ registerNudge(settle.api, run, void 0, nested);
1232
+ registerCloseNudge(settle.api, run, process.env, nested);
1233
+ registerRefuteNudge(settle.api, run, nested);
1234
+ settle.flush();
1209
1235
  }
1210
1236
  function serveCtl(pi) {
1211
1237
  const sock = process.env.MU_CTL_SOCK;