@mu-crew/mu 3.7.0 → 3.8.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.
@@ -23,6 +23,7 @@ var CTL_OPS = [
23
23
  "send",
24
24
  "wait",
25
25
  "abort",
26
+ "interrupt",
26
27
  "fresh",
27
28
  "command"
28
29
  ];
@@ -152,6 +153,23 @@ ${agent.lastText ?? ""}`;
152
153
  return `${who}: mu agent wait gave no result. ${fate}${ws}${tailBlock}`;
153
154
  }
154
155
  }
156
+ var RECORD_NOTE_MAX = 4e3;
157
+ var RECORD_FALLBACK_TAIL = 1500;
158
+ var VERDICT_LINE = /^[\s>*_`-]*VERDICT:/;
159
+ var EVIDENCE_LINE = /^[\s>*_`-]*EVIDENCE:/;
160
+ function recordNote(label, name, outcome, lastText, elapsedMs) {
161
+ if (outcome !== "done" || !lastText?.trim())
162
+ return `REFUTER ${label}: no verdict (${outcome === "done" ? "empty" : outcome ?? "no result"})`;
163
+ const lines = lastText.trimEnd().split("\n");
164
+ let at = -1;
165
+ for (let i = lines.length - 1; i >= 0 && at < 0; i--)
166
+ if (VERDICT_LINE.test(lines[i] ?? "")) at = i;
167
+ const body = at >= 0 ? [lines[at] ?? "", ...lines.slice(at + 1).filter((l) => EVIDENCE_LINE.test(l))].join("\n") : `NO VERDICT LINE: ${lastText.trimEnd().slice(-RECORD_FALLBACK_TAIL)}`;
168
+ const note = `REFUTER ${label} (${name}, ${formatElapsed(elapsedMs)}):
169
+ ${body}`;
170
+ const mark = "\n[truncated]";
171
+ return note.length <= RECORD_NOTE_MAX ? note : note.slice(0, RECORD_NOTE_MAX - mark.length) + mark;
172
+ }
155
173
  function labelStem(label) {
156
174
  if (typeof label !== "string") return void 0;
157
175
  const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "");
@@ -200,7 +218,7 @@ function registerDelegate(pi, run = defaultRunner()) {
200
218
  const q = queue.shift();
201
219
  if (!q) break;
202
220
  starting++;
203
- void start(q.params, void 0, q.ctx, q.handle).then(
221
+ void start(q.params, void 0, q.ctx, q.handle, q.record).then(
204
222
  () => {
205
223
  },
206
224
  (e) => {
@@ -288,7 +306,35 @@ function registerDelegate(pi, run = defaultRunner()) {
288
306
  };
289
307
  }
290
308
  }
291
- async function deliver(name, wait, keep, extras) {
309
+ const recordLabel = (params, name) => typeof params.label === "string" && params.label.trim() ? params.label.trim() : name;
310
+ async function resolveRecord(raw) {
311
+ if (raw === void 0) return void 0;
312
+ const r = raw;
313
+ const task = typeof r?.task === "string" ? r.task.trim() : "";
314
+ if (!task) throw new Error("mu_delegate: record.task must name a task");
315
+ const ws = typeof r?.workstream === "string" ? r.workstream.trim() : "";
316
+ const shown = await mu(["task", "show", task, ...ws ? ["-w", ws] : [], "--json"]);
317
+ const out = json(shown);
318
+ const t = out?.task;
319
+ if (shown.code !== 0 || typeof t?.name !== "string" || typeof t.workstreamName !== "string") {
320
+ const why = typeof out?.message === "string" ? out.message : (shown.stderr || shown.stdout).trim();
321
+ throw new Error(
322
+ `mu_delegate: record task ${ws ? `${ws}/` : ""}${task} not found (exit ${shown.code}): ${why}. Nothing spawned.`
323
+ );
324
+ }
325
+ return { workstream: t.workstreamName, task: t.name };
326
+ }
327
+ async function writeRecord(rec, name, text) {
328
+ const where = `${rec.workstream}/${rec.task}`;
329
+ try {
330
+ const r = await mu(["task", "note", rec.task, "-w", rec.workstream, "--author", name, text]);
331
+ if (r.code === 0) return `Recorded on ${where} as a note.`;
332
+ return `Recording on ${where} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}. Record it yourself.`;
333
+ } catch (e) {
334
+ return `Recording on ${where} failed: ${e instanceof Error ? e.message : String(e)}. Record it yourself.`;
335
+ }
336
+ }
337
+ async function deliver(name, wait, keep, extras, record) {
292
338
  const agent = json(wait)?.agents?.[0];
293
339
  const outcome = agent?.outcome;
294
340
  const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" || outcome === "error" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
@@ -303,10 +349,18 @@ function registerDelegate(pi, run = defaultRunner()) {
303
349
  failed.add(name);
304
350
  showStatus();
305
351
  }
352
+ const recorded = record ? await writeRecord(
353
+ record.target,
354
+ name,
355
+ recordNote(record.label, name, outcome, agent?.lastText, extras.elapsedMs ?? 0)
356
+ ) : void 0;
357
+ const message = delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} });
306
358
  await pi.sendMessage(
307
359
  {
308
360
  customType: DELEGATE_MESSAGE_TYPE,
309
- content: delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} }),
361
+ content: recorded ? `${message}
362
+
363
+ ${recorded}` : message,
310
364
  display: true,
311
365
  details: {
312
366
  name,
@@ -321,7 +375,7 @@ function registerDelegate(pi, run = defaultRunner()) {
321
375
  { deliverAs: "followUp", triggerTurn: true }
322
376
  );
323
377
  }
324
- async function start(params, signal, ctx, queuedAs) {
378
+ async function start(params, signal, ctx, queuedAs, record) {
325
379
  let slotHeld = true;
326
380
  const freeSlot = () => {
327
381
  if (slotHeld) {
@@ -347,7 +401,11 @@ function registerDelegate(pi, run = defaultRunner()) {
347
401
  if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
348
402
  }
349
403
  const { name, attach, workspace } = await spawn(params, cwd, signal);
350
- const entry = { abort: new AbortController(), cancelling: false };
404
+ const entry = {
405
+ abort: new AbortController(),
406
+ cancelling: false,
407
+ ...record ? { record: { target: record, label: recordLabel(params, name) } } : {}
408
+ };
351
409
  inflight.set(name, entry);
352
410
  freeSlot();
353
411
  showStatus();
@@ -392,13 +450,19 @@ ${task}` : task,
392
450
  const settle = async (r) => {
393
451
  forget(name);
394
452
  try {
395
- await deliver(name, r, keep, {
396
- timeoutS,
397
- elapsedMs: Date.now() - started,
398
- ...queuedAs ? { queuedAs } : {},
399
- ...attach ? { attach } : {},
400
- ...workspace ? { workspace } : {}
401
- });
453
+ await deliver(
454
+ name,
455
+ r,
456
+ keep,
457
+ {
458
+ timeoutS,
459
+ elapsedMs: Date.now() - started,
460
+ ...queuedAs ? { queuedAs } : {},
461
+ ...attach ? { attach } : {},
462
+ ...workspace ? { workspace } : {}
463
+ },
464
+ entry.record
465
+ );
402
466
  } catch (e) {
403
467
  await Promise.resolve(
404
468
  pi.sendMessage(
@@ -443,7 +507,7 @@ ${task}` : task,
443
507
  pi.registerTool({
444
508
  name: DELEGATE_TOOL,
445
509
  label: "mu delegate",
446
- description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation; call it several times to fan out in parallel (at most ${delegateMax()} run at once; up to ${DELEGATE_QUEUE_FACTOR * delegateMax()} more are queued and start as slots free). The subagent starts with no context, in your working directory: put everything it needs in task. The call returns at once and the answer arrives later as a follow-up message: carry on with other work, or end your turn if your next step needs the answer (the follow-up resumes you). The pane closes after a clean finish; keep: true leaves it open.`,
510
+ description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation, or a refuter of a claim, plan or fix before you act on it; call it several times to fan out in parallel (at most ${delegateMax()} run at once; up to ${DELEGATE_QUEUE_FACTOR * delegateMax()} more are queued and start as slots free). The subagent starts with no context, in your working directory: put everything it needs in task. The call returns at once and the answer arrives later as a follow-up message: carry on with other work, or end your turn if your next step needs the answer (the follow-up resumes you). The pane closes after a clean finish; keep: true leaves it open. When the check judges a mu task, set record so its verdict lands on that task; leave it out when there is no task.`,
447
511
  promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
448
512
  parameters: {
449
513
  type: "object",
@@ -477,6 +541,16 @@ ${task}` : task,
477
541
  keep: {
478
542
  type: "boolean",
479
543
  description: "Keep the pane after it finishes, to talk to it again"
544
+ },
545
+ record: {
546
+ type: "object",
547
+ description: "When the answer arrives, write its VERDICT/EVIDENCE onto this task as a note (for a check that judges a mu task; omit with no task); task may be <ws>/<task>.",
548
+ properties: {
549
+ task: { type: "string" },
550
+ workstream: { type: "string" }
551
+ },
552
+ required: ["task"],
553
+ additionalProperties: false
480
554
  }
481
555
  },
482
556
  required: ["task"],
@@ -495,10 +569,11 @@ ${task}` : task,
495
569
  const cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
496
570
  if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
497
571
  }
572
+ const record = await resolveRecord(params.record);
498
573
  const max = delegateMax();
499
574
  if (inflight.size + starting < max) {
500
575
  starting++;
501
- return start(params, signal, ctx);
576
+ return start(params, signal, ctx, void 0, record);
502
577
  }
503
578
  if (queue.length >= DELEGATE_QUEUE_FACTOR * max)
504
579
  throw new Error(
@@ -511,7 +586,8 @@ ${task}` : task,
511
586
  ...params,
512
587
  cwd: typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd()
513
588
  },
514
- ctx
589
+ ctx,
590
+ ...record ? { record } : {}
515
591
  });
516
592
  showStatus();
517
593
  return {
@@ -544,10 +620,13 @@ ${task}` : task,
544
620
  if (!name) throw new Error("mu_delegate_cancel needs a name");
545
621
  const qi = queue.findIndex((q) => q.handle === name);
546
622
  if (qi >= 0) {
547
- queue.splice(qi, 1);
623
+ const [q] = queue.splice(qi, 1);
548
624
  showStatus();
625
+ const recorded = q?.record ? ` ${await writeRecord(q.record, name, recordNote(recordLabel(q.params, name), name, "cancelled", void 0, 0))}` : "";
549
626
  return {
550
- content: [{ type: "text", text: `Dropped ${name} from the queue; it never started.` }],
627
+ content: [
628
+ { type: "text", text: `Dropped ${name} from the queue; it never started.${recorded}` }
629
+ ],
551
630
  details: { name, workstream: W }
552
631
  };
553
632
  }
@@ -569,6 +648,8 @@ ${task}` : task,
569
648
  }
570
649
  failed.delete(name);
571
650
  let text = `Aborted ${W}/${name}.`;
651
+ if (entry?.record)
652
+ text += ` ${await writeRecord(entry.record.target, name, recordNote(entry.record.label, name, "cancelled", void 0, 0))}`;
572
653
  if (params.keep === true) text += " Pane kept.";
573
654
  else {
574
655
  const c = await mu(["agent", "close", name, "-w", W]);
@@ -605,9 +686,14 @@ import { join as join2 } from "path";
605
686
  var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
606
687
  var NUDGE_LOG_KIND = "nudge";
607
688
  var CLOSE_NUDGE_MESSAGE_TYPE = "mu-close-task";
689
+ var REFUTE_NUDGE_MESSAGE_TYPE = "mu-refute-brief";
608
690
  var MARK_START = "<!-- mu:keep-driving -->";
609
691
  var MARK_END = "<!-- /mu:keep-driving -->";
610
692
  var WAIT_HINT_MAX = 8;
693
+ var notNested = () => false;
694
+ function noUI(ctx) {
695
+ return ctx?.hasUI === false;
696
+ }
611
697
  function nudgeEnabled(env = process.env) {
612
698
  return env.MU_NUDGE !== "0";
613
699
  }
@@ -627,8 +713,7 @@ function readSkill() {
627
713
  }
628
714
  function dispatchedWorkstreams(command) {
629
715
  const found = /* @__PURE__ */ new Set();
630
- for (const seg of command.split(/&&|\|\||;|\n|\|/)) {
631
- const words = seg.trim().split(/\s+/);
716
+ for (const words of shellSegments(command)) {
632
717
  const at = words.findIndex((w) => w === "mu" || w.endsWith("/mu"));
633
718
  if (at < 0) continue;
634
719
  const [noun, verb] = [words[at + 1], words[at + 2]];
@@ -638,19 +723,78 @@ function dispatchedWorkstreams(command) {
638
723
  let ws = "";
639
724
  for (let k = at + 3; k < words.length; k++) {
640
725
  const w = words[k] ?? "";
641
- if (w === "-w" || w === "--workstream") ws = unquote(words[k + 1] ?? "");
642
- else if (w.startsWith("--workstream=")) ws = unquote(w.slice("--workstream=".length));
726
+ if (w === "-w" || w === "--workstream") ws = words[k + 1] ?? "";
727
+ else if (w.startsWith("--workstream=")) ws = w.slice("--workstream=".length);
643
728
  }
644
729
  if (ws === "" && isClaimFor) {
645
730
  const ref = words[at + 3] ?? "";
646
- if (ref.includes("/")) ws = unquote(ref.split("/")[0] ?? "");
731
+ if (ref.includes("/")) ws = ref.split("/")[0] ?? "";
647
732
  }
648
733
  if (ws !== "scratch") found.add(ws);
649
734
  }
650
735
  return found.size > 0 ? [...found] : void 0;
651
736
  }
652
- function unquote(s) {
653
- return s.replace(/^['"]|['"]$/g, "");
737
+ var CLAIM_VALUE_OPTS = /* @__PURE__ */ new Set(["--for", "-f", "-w", "--workstream", "--evidence", "--actor"]);
738
+ function shellSegments(command) {
739
+ const segs = [];
740
+ let words = [];
741
+ let word;
742
+ const endWord = () => {
743
+ if (word !== void 0) words.push(word);
744
+ word = void 0;
745
+ };
746
+ const endSeg = () => {
747
+ endWord();
748
+ if (words.length > 0) segs.push(words);
749
+ words = [];
750
+ };
751
+ for (let i = 0; i < command.length; i++) {
752
+ const c = command[i] ?? "";
753
+ if (c === "'" || c === '"') {
754
+ const close = command.indexOf(c, i + 1);
755
+ const end = close < 0 ? command.length : close;
756
+ word = (word ?? "") + command.slice(i + 1, end);
757
+ i = end;
758
+ } else if (c === "\\" && i + 1 < command.length) {
759
+ word = (word ?? "") + command[++i];
760
+ } else if (c === ";" || c === "|" || c === "\n" || c === "&" && command[i + 1] === "&") {
761
+ if (c !== ";" && c !== "\n" && command[i + 1] === c) i++;
762
+ endSeg();
763
+ } else if (/\s/.test(c)) {
764
+ endWord();
765
+ } else {
766
+ word = (word ?? "") + c;
767
+ }
768
+ }
769
+ endSeg();
770
+ return segs;
771
+ }
772
+ function dispatchedTasks(command) {
773
+ const out = [];
774
+ for (const words of shellSegments(command)) {
775
+ if (words[0] !== "mu" && !words[0]?.endsWith("/mu")) continue;
776
+ if (words[1] !== "task" || words[2] !== "claim") continue;
777
+ let ws = "";
778
+ let id;
779
+ let isFor = false;
780
+ for (let k = 3; k < words.length; k++) {
781
+ const w = words[k] ?? "";
782
+ const [opt, eq] = w.startsWith("--") && w.includes("=") ? w.split("=", 2) : [w, void 0];
783
+ if (opt === "--for" || opt === "-f") isFor = true;
784
+ if (opt === "-w" || opt === "--workstream") ws = eq ?? words[k + 1] ?? "";
785
+ if (CLAIM_VALUE_OPTS.has(opt ?? "")) {
786
+ if (eq === void 0) k++;
787
+ } else if (id === void 0 && !w.startsWith("-")) id = w;
788
+ }
789
+ if (!isFor || id === void 0) continue;
790
+ const slash = id.indexOf("/");
791
+ if (slash >= 0) {
792
+ if (ws === "") ws = id.slice(0, slash);
793
+ id = id.slice(slash + 1);
794
+ }
795
+ if (ws !== "scratch") out.push({ ws, id });
796
+ }
797
+ return out;
654
798
  }
655
799
  async function inProgress(run, workstreams) {
656
800
  const named = workstreams.filter((w) => w !== "");
@@ -690,7 +834,7 @@ function bashCommand(event) {
690
834
  const e = event;
691
835
  return e.toolName === "bash" && typeof e.input?.command === "string" ? e.input.command : void 0;
692
836
  }
693
- function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
837
+ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill(), nested = notNested) {
694
838
  if (!nudgeEnabled()) return;
695
839
  const rule = skillMd === void 0 ? void 0 : keepDrivingRule(skillMd);
696
840
  if (rule === void 0) return;
@@ -706,7 +850,7 @@ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
706
850
  for (const ws of dispatchedWorkstreams(cmd) ?? []) armed.add(ws);
707
851
  });
708
852
  pi.on("agent_before_settle", async (event) => {
709
- if (fired || armed.size === 0) return;
853
+ if (fired || armed.size === 0 || nested()) return;
710
854
  if (outcomeOf(event) !== "completed") return;
711
855
  fired = true;
712
856
  const refs = await inProgress(run, [...armed]);
@@ -755,7 +899,7 @@ function closeNudgeText(workstream, names) {
755
899
  "If you are blocked or need an answer, say so in one line and stop; the orchestrator will read your pane."
756
900
  ].join("\n");
757
901
  }
758
- function registerCloseNudge(pi, run = defaultRunner(), env = process.env) {
902
+ function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested = notNested) {
759
903
  if (!nudgeEnabled(env)) return;
760
904
  const me = workerIdentity(env);
761
905
  if (me === void 0) return;
@@ -763,8 +907,8 @@ function registerCloseNudge(pi, run = defaultRunner(), env = process.env) {
763
907
  pi.on("input", () => {
764
908
  fired = false;
765
909
  });
766
- pi.on("agent_before_settle", async (event) => {
767
- if (fired) return;
910
+ pi.on("agent_before_settle", async (event, ctx) => {
911
+ if (fired || nested() || noUI(ctx)) return;
768
912
  if (outcomeOf(event) !== "completed") return;
769
913
  fired = true;
770
914
  const names = await ownedInProgress(run, me.agent, me.workstream);
@@ -790,9 +934,67 @@ function registerCloseNudge(pi, run = defaultRunner(), env = process.env) {
790
934
  };
791
935
  });
792
936
  }
937
+ var REFUTE_MARK = /^(REFUTER |VERDICT:|REFUTE-EXEMPT:)/m;
938
+ async function briefRefuted(run, ws, id) {
939
+ const r = await run(["task", "notes", id, ...ws ? ["-w", ws] : [], "--json"]);
940
+ if (r.code !== 0) return void 0;
941
+ try {
942
+ const json = JSON.parse(r.stdout);
943
+ return (json.items ?? []).some((n) => REFUTE_MARK.test(n.content ?? ""));
944
+ } catch {
945
+ return void 0;
946
+ }
947
+ }
948
+ function refuteNudgeText(refs) {
949
+ return `[mu] dispatched ${refs.join(", ")} with an unrefuted brief: refute it (delegate, record) or note REFUTE-EXEMPT: <why>`;
950
+ }
951
+ function registerRefuteNudge(pi, run = defaultRunner(), nested = notNested) {
952
+ if (!nudgeEnabled()) return;
953
+ const armed = /* @__PURE__ */ new Map();
954
+ let fired = false;
955
+ pi.on("input", () => {
956
+ armed.clear();
957
+ fired = false;
958
+ });
959
+ pi.on("tool_call", (event) => {
960
+ const cmd = bashCommand(event);
961
+ if (cmd === void 0) return;
962
+ for (const t of dispatchedTasks(cmd)) {
963
+ if (!t.id.startsWith("review_")) armed.set(`${t.ws}/${t.id}`, t);
964
+ }
965
+ });
966
+ pi.on("agent_before_settle", async (event, ctx) => {
967
+ if (fired || armed.size === 0 || nested() || noUI(ctx)) return;
968
+ if (outcomeOf(event) !== "completed") return;
969
+ fired = true;
970
+ const refs = [];
971
+ for (const [ref, t] of armed) {
972
+ if (await briefRefuted(run, t.ws, t.id) === false) refs.push(ref);
973
+ }
974
+ if (refs.length === 0) return;
975
+ const logWs = [...armed.values()].find((t) => t.ws !== "")?.ws;
976
+ await run([
977
+ "log",
978
+ ...logWs ? ["-w", logWs] : [],
979
+ "--kind",
980
+ NUDGE_LOG_KIND,
981
+ `refute: ${refs.join(", ")}`
982
+ ]);
983
+ return {
984
+ entries: [
985
+ {
986
+ type: "custom_message",
987
+ customType: REFUTE_NUDGE_MESSAGE_TYPE,
988
+ content: refuteNudgeText(refs),
989
+ display: true
990
+ }
991
+ ]
992
+ };
993
+ });
994
+ }
793
995
 
794
996
  // extension/mu-pi.ts
795
- var EXT_VERSION = true ? "3.7.0" : void 0;
997
+ var EXT_VERSION = true ? "3.8.0" : void 0;
796
998
  var FRESH_COMMAND = "mu-fresh";
797
999
  var FRESH_TIMEOUT_MS = 3e4;
798
1000
  function commandName(name) {
@@ -931,16 +1133,17 @@ function registerPrompts(pi) {
931
1133
  return { promptPaths: paths };
932
1134
  });
933
1135
  }
934
- function muPi(pi) {
935
- registerDelegate(pi);
1136
+ function muPi(pi, run = defaultRunner()) {
1137
+ registerDelegate(pi, run);
936
1138
  registerPrompts(pi);
937
- registerNudge(pi);
938
- registerCloseNudge(pi);
939
- serveCtl(pi);
1139
+ const nested = serveCtl(pi);
1140
+ registerNudge(pi, run, void 0, nested);
1141
+ registerCloseNudge(pi, run, process.env, nested);
1142
+ registerRefuteNudge(pi, run, nested);
940
1143
  }
941
1144
  function serveCtl(pi) {
942
1145
  const sock = process.env.MU_CTL_SOCK;
943
- if (!sock) return;
1146
+ if (!sock) return () => false;
944
1147
  const sockPath = sock;
945
1148
  const piVersion = detectPiVersion();
946
1149
  const map = sharedMap();
@@ -1130,10 +1333,28 @@ function serveCtl(pi) {
1130
1333
  if (!g.ctx) return fail("no pi context yet");
1131
1334
  g.ctx.abort();
1132
1335
  return { v: V, ok: true, state: g.state };
1336
+ case "interrupt":
1337
+ return interrupt(req, conn);
1133
1338
  default:
1134
1339
  return { ...fail(`${UNKNOWN_OP_PREFIX}${String(req.op)}`), ops: [...CTL_OPS] };
1135
1340
  }
1136
1341
  }
1342
+ async function interrupt(req, conn) {
1343
+ const text = str(req, "text");
1344
+ if (text === void 0) return fail("interrupt needs a string text");
1345
+ const wasBusy = !(g.ctx?.isIdle() ?? g.state === "idle");
1346
+ const { pending } = status();
1347
+ if (wasBusy) {
1348
+ if (!g.ctx) return fail("no pi context yet");
1349
+ const settled = wait(g.runs, num(req, "timeoutMs"), conn);
1350
+ g.ctx.abort();
1351
+ const w = await settled;
1352
+ if (!w.ok) return w;
1353
+ }
1354
+ const before = status();
1355
+ await g.pi.sendUserMessage(text);
1356
+ return { v: V, ok: true, ...before, pending, wasBusy };
1357
+ }
1137
1358
  const settledRun = () => ({
1138
1359
  lastText: g.lastText,
1139
1360
  ...g.lastError !== void 0 ? { lastError: g.lastError } : {}
@@ -1196,8 +1417,9 @@ function serveCtl(pi) {
1196
1417
  async function bind() {
1197
1418
  mkdirSync(dirname(sockPath), { recursive: true, mode: 448 });
1198
1419
  if (await socketIsLive(sockPath)) {
1420
+ g.nested = true;
1199
1421
  process.stderr.write(
1200
- `mu: control socket ${sockPath} is served by another pi; this pi will not take it
1422
+ `mu: control socket ${sockPath} is served by another pi; nested pi: mu agent features off
1201
1423
  `
1202
1424
  );
1203
1425
  return;
@@ -1219,14 +1441,18 @@ function serveCtl(pi) {
1219
1441
  } catch (e) {
1220
1442
  s.close();
1221
1443
  if (e.code !== "EEXIST") throw e;
1222
- process.stderr.write(`mu: control socket ${sockPath} was taken by another pi
1223
- `);
1444
+ g.nested = true;
1445
+ process.stderr.write(
1446
+ `mu: control socket ${sockPath} was taken by another pi; nested pi: mu agent features off
1447
+ `
1448
+ );
1224
1449
  return;
1225
1450
  } finally {
1226
1451
  rmSync(tmp, { force: true });
1227
1452
  }
1228
1453
  g.server = s;
1229
1454
  g.ino = inodeOf(sockPath);
1455
+ g.nested = false;
1230
1456
  }
1231
1457
  pi.on("session_start", async (e, c) => {
1232
1458
  g.ctx = c;
@@ -1258,6 +1484,7 @@ function serveCtl(pi) {
1258
1484
  await new Promise((resolve) => s.close(() => resolve()));
1259
1485
  if (ino !== void 0 && inodeOf(sockPath) === ino) rmSync(sockPath, { force: true });
1260
1486
  });
1487
+ return () => g.nested === true;
1261
1488
  }
1262
1489
  export {
1263
1490
  COMMAND_TIMEOUT_MS,