@mu-crew/mu 3.4.0 → 3.6.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.
@@ -51,11 +51,18 @@ var DELEGATE_CANCEL_TOOL = "mu_delegate_cancel";
51
51
  var DELEGATE_MESSAGE_TYPE = "mu-delegate";
52
52
  var DELEGATE_WORKSTREAM = "scratch";
53
53
  var DELEGATE_STATUS_KEY = "mu-delegate";
54
- function delegateStatus(n) {
55
- return n > 0 ? `${n} delegate${n === 1 ? "" : "s"} running` : void 0;
54
+ var PENDING_CHANNEL = "murmur:pending";
55
+ function delegateStatus(running, starting = 0, queued = 0, failed = 0) {
56
+ const parts = [];
57
+ if (running > 0) parts.push(`${running} delegate${running === 1 ? "" : "s"} running`);
58
+ if (starting > 0) parts.push(`${starting} starting`);
59
+ if (queued > 0) parts.push(`${queued} queued`);
60
+ if (failed > 0) parts.push(`${failed} failed`);
61
+ return parts.length > 0 ? parts.join(", ") : void 0;
56
62
  }
57
63
  var DELEGATE_TIMEOUT_S = 3600;
58
64
  var DELEGATE_MAX_DEFAULT = 16;
65
+ var DELEGATE_QUEUE_FACTOR = 4;
59
66
  function delegateMax(env = process.env) {
60
67
  const n = Number(env.MU_DELEGATE_MAX);
61
68
  return Number.isInteger(n) && n > 0 ? n : DELEGATE_MAX_DEFAULT;
@@ -106,7 +113,7 @@ function formatElapsed(ms) {
106
113
  }
107
114
  function delegateMessage(name, agent, pane, x = {}) {
108
115
  const timeoutS = x.timeoutS ?? DELEGATE_TIMEOUT_S;
109
- const who = `Delegate ${W}/${name}`;
116
+ const who = `Delegate ${W}/${name}${x.queuedAs ? ` (queued as ${x.queuedAs})` : ""}`;
110
117
  const took = x.elapsedMs !== void 0 ? ` after ${formatElapsed(x.elapsedMs)}` : "";
111
118
  const fate = pane.closed ? "Pane closed." : `Pane kept (${pane.why}).`;
112
119
  const look = [
@@ -125,6 +132,13 @@ ${x.tail.trimEnd()}` : "";
125
132
  ${agent.lastText ?? ""}`;
126
133
  case "empty":
127
134
  return `${who} finished${took} without a text answer. ${fate}${ws}${tailBlock}`;
135
+ case "error":
136
+ return [
137
+ `${who} stopped on an API error${took}, after pi's own retries: ${(agent.lastError ?? "unknown error").replace(/[.\s]+$/, "")}. ${fate}${ws}`,
138
+ "Decide: re-issue the call, or record the check as UNVERIFIED.",
139
+ ...look,
140
+ `Close: mu agent close ${name} -w ${W}`
141
+ ].join("\n");
128
142
  case "died":
129
143
  return `${who} died${took} before answering. ${fate}${ws}${tailBlock}`;
130
144
  case "timeout":
@@ -147,11 +161,28 @@ function registerDelegate(pi, run = defaultRunner()) {
147
161
  if (!delegateEnabled()) return;
148
162
  const inflight = /* @__PURE__ */ new Map();
149
163
  let starting = 0;
164
+ const queue = [];
165
+ let queueSeq = 0;
166
+ const failed = /* @__PURE__ */ new Set();
150
167
  const reserved = /* @__PURE__ */ new Set();
151
168
  let ui;
169
+ let lastPending = 0;
170
+ const reportPending = () => {
171
+ const count = inflight.size + starting + queue.length;
172
+ if (count === lastPending) return;
173
+ lastPending = count;
174
+ try {
175
+ pi.events.emit(PENDING_CHANNEL, { source: DELEGATE_TOOL, count });
176
+ } catch {
177
+ }
178
+ };
152
179
  const showStatus = () => {
180
+ reportPending();
153
181
  try {
154
- ui?.setStatus?.(DELEGATE_STATUS_KEY, delegateStatus(inflight.size));
182
+ ui?.setStatus?.(
183
+ DELEGATE_STATUS_KEY,
184
+ delegateStatus(inflight.size, starting, queue.length, failed.size)
185
+ );
155
186
  } catch {
156
187
  }
157
188
  };
@@ -161,8 +192,34 @@ function registerDelegate(pi, run = defaultRunner()) {
161
192
  const forget = (name) => {
162
193
  inflight.delete(name);
163
194
  reserved.delete(name);
195
+ drain();
164
196
  showStatus();
165
197
  };
198
+ function drain() {
199
+ while (queue.length > 0 && inflight.size + starting < delegateMax()) {
200
+ const q = queue.shift();
201
+ if (!q) break;
202
+ starting++;
203
+ void start(q.params, void 0, q.ctx, q.handle).then(
204
+ () => {
205
+ },
206
+ (e) => {
207
+ void Promise.resolve(
208
+ pi.sendMessage(
209
+ {
210
+ customType: DELEGATE_MESSAGE_TYPE,
211
+ content: `Queued delegate ${q.handle} could not start: ${e instanceof Error ? e.message : String(e)}`,
212
+ display: true,
213
+ details: { name: q.handle, workstream: W, outcome: null, closed: false }
214
+ },
215
+ { deliverAs: "followUp", triggerTurn: true }
216
+ )
217
+ ).catch(() => {
218
+ });
219
+ }
220
+ );
221
+ }
222
+ }
166
223
  const mu = run;
167
224
  const json = (r) => {
168
225
  try {
@@ -179,6 +236,10 @@ function registerDelegate(pi, run = defaultRunner()) {
179
236
  const taken = new Set(
180
237
  Array.isArray(agents) ? agents.map((a) => a.name).filter((n) => n) : []
181
238
  );
239
+ if (Array.isArray(agents)) {
240
+ for (const f of [...failed]) if (!taken.has(f)) failed.delete(f);
241
+ showStatus();
242
+ }
182
243
  for (let n = 1; ; n++) {
183
244
  const name = stem ? n === 1 ? `delegate-${stem}` : `delegate-${stem}-${n}` : `delegate-${n}`;
184
245
  if (!taken.has(name) && !reserved.has(name)) {
@@ -230,7 +291,7 @@ function registerDelegate(pi, run = defaultRunner()) {
230
291
  async function deliver(name, wait, keep, extras) {
231
292
  const agent = json(wait)?.agents?.[0];
232
293
  const outcome = agent?.outcome;
233
- const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
294
+ const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" || outcome === "error" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
234
295
  let pane = { closed: false, why: "keep: true" };
235
296
  if (outcome !== "done" && outcome !== "empty")
236
297
  pane = { closed: false, why: "kept as evidence" };
@@ -238,6 +299,10 @@ function registerDelegate(pi, run = defaultRunner()) {
238
299
  const c = await mu(["agent", "close", name, "-w", W]);
239
300
  pane = c.code === 0 ? { closed: true } : { closed: false, why: `close refused: ${(c.stderr || c.stdout).trim()}` };
240
301
  }
302
+ if (outcome === "error" && !pane.closed) {
303
+ failed.add(name);
304
+ showStatus();
305
+ }
241
306
  await pi.sendMessage(
242
307
  {
243
308
  customType: DELEGATE_MESSAGE_TYPE,
@@ -256,10 +321,106 @@ function registerDelegate(pi, run = defaultRunner()) {
256
321
  { deliverAs: "followUp", triggerTurn: true }
257
322
  );
258
323
  }
324
+ async function start(params, signal, ctx, queuedAs) {
325
+ let slotHeld = true;
326
+ const freeSlot = () => {
327
+ if (slotHeld) {
328
+ starting--;
329
+ showStatus();
330
+ }
331
+ slotHeld = false;
332
+ };
333
+ showStatus();
334
+ try {
335
+ const task = typeof params.task === "string" ? params.task.trim() : "";
336
+ const brief = typeof params.brief === "string" ? params.brief.trim() : "";
337
+ const keep = params.keep === true;
338
+ const t = params.timeout;
339
+ if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
340
+ throw new Error(
341
+ `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
342
+ );
343
+ const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
344
+ let cwd;
345
+ if (params.workspace !== true) {
346
+ cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
347
+ if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
348
+ }
349
+ const { name, attach, workspace } = await spawn(params, cwd, signal);
350
+ const entry = { abort: new AbortController(), cancelling: false };
351
+ inflight.set(name, entry);
352
+ freeSlot();
353
+ showStatus();
354
+ const started = Date.now();
355
+ const waiting = mu(
356
+ ["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
357
+ entry.abort.signal
358
+ );
359
+ const sent = await mu(["agent", "send", name, brief ? `${brief}
360
+
361
+ ${task}` : task, "-w", W]);
362
+ if (sent.code !== 0) {
363
+ entry.abort.abort();
364
+ forget(name);
365
+ const c = await mu(["agent", "close", name, "-w", W]);
366
+ const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
367
+ throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
368
+ }
369
+ const settle = async (r) => {
370
+ forget(name);
371
+ try {
372
+ await deliver(name, r, keep, {
373
+ timeoutS,
374
+ elapsedMs: Date.now() - started,
375
+ ...queuedAs ? { queuedAs } : {},
376
+ ...attach ? { attach } : {},
377
+ ...workspace ? { workspace } : {}
378
+ });
379
+ } catch (e) {
380
+ await Promise.resolve(
381
+ pi.sendMessage(
382
+ {
383
+ customType: DELEGATE_MESSAGE_TYPE,
384
+ content: `Delegate ${W}/${name} settled, but delivering its answer failed: ${e instanceof Error ? e.message : String(e)}. Read it with: mu agent read ${name} -n 50 -w ${W}`,
385
+ display: true,
386
+ details: { name, workstream: W, outcome: null, closed: false }
387
+ },
388
+ { deliverAs: "followUp", triggerTurn: true }
389
+ )
390
+ ).catch(() => {
391
+ });
392
+ }
393
+ };
394
+ void waiting.then(async (r) => {
395
+ if (inflight.get(name) !== entry) return;
396
+ if (entry.cancelling) entry.parked = () => settle(r);
397
+ else await settle(r);
398
+ });
399
+ const lines = [
400
+ `Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
401
+ workspace ? `Workspace: ${workspace}` : void 0,
402
+ attach ? `Attach: ${attach}` : void 0,
403
+ `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
404
+ ].filter((x) => x !== void 0);
405
+ return {
406
+ content: [{ type: "text", text: lines.join("\n") }],
407
+ details: {
408
+ name,
409
+ workstream: W,
410
+ attach: attach ?? null,
411
+ workspace: workspace ?? null,
412
+ keep
413
+ }
414
+ };
415
+ } finally {
416
+ freeSlot();
417
+ if (queuedAs) drain();
418
+ }
419
+ }
259
420
  pi.registerTool({
260
421
  name: DELEGATE_TOOL,
261
422
  label: "mu delegate",
262
- 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()} in flight). 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.`,
423
+ 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.`,
263
424
  promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
264
425
  parameters: {
265
426
  type: "object",
@@ -302,106 +463,43 @@ function registerDelegate(pi, run = defaultRunner()) {
302
463
  track(ctx);
303
464
  const task = typeof params.task === "string" ? params.task.trim() : "";
304
465
  if (!task) throw new Error("mu_delegate needs a non-empty task");
305
- const max = delegateMax();
306
- if (inflight.size + starting >= max)
466
+ const t = params.timeout;
467
+ if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
307
468
  throw new Error(
308
- `mu_delegate: ${inflight.size + starting} delegates already running (MU_DELEGATE_MAX=${max}). End your turn; issue the rest as answers arrive (each answer resumes you).`
469
+ `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
309
470
  );
310
- starting++;
311
- let slotHeld = true;
312
- const freeSlot = () => {
313
- if (slotHeld) starting--;
314
- slotHeld = false;
315
- };
316
- try {
317
- const brief = typeof params.brief === "string" ? params.brief.trim() : "";
318
- const keep = params.keep === true;
319
- const t = params.timeout;
320
- if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
321
- throw new Error(
322
- `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
323
- );
324
- const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
325
- let cwd;
326
- if (params.workspace !== true) {
327
- cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
328
- if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
329
- }
330
- const { name, attach, workspace } = await spawn(params, cwd, signal);
331
- const entry = { abort: new AbortController(), cancelling: false };
332
- inflight.set(name, entry);
333
- freeSlot();
334
- showStatus();
335
- const started = Date.now();
336
- const waiting = mu(
337
- ["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
338
- entry.abort.signal
471
+ if (params.workspace !== true) {
472
+ const cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
473
+ if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
474
+ }
475
+ const max = delegateMax();
476
+ if (inflight.size + starting < max) {
477
+ starting++;
478
+ return start(params, signal, ctx);
479
+ }
480
+ if (queue.length >= DELEGATE_QUEUE_FACTOR * max)
481
+ throw new Error(
482
+ `mu_delegate: ${inflight.size + starting} running and ${queue.length} queued (MU_DELEGATE_MAX=${max}). End your turn; issue the rest as answers arrive (each answer resumes you).`
339
483
  );
340
- const sent = await mu([
341
- "agent",
342
- "send",
343
- name,
344
- brief ? `${brief}
345
-
346
- ${task}` : task,
347
- "-w",
348
- W
349
- ]);
350
- if (sent.code !== 0) {
351
- entry.abort.abort();
352
- forget(name);
353
- const c = await mu(["agent", "close", name, "-w", W]);
354
- const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
355
- throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
356
- }
357
- const settle = async (r) => {
358
- forget(name);
359
- try {
360
- await deliver(name, r, keep, {
361
- timeoutS,
362
- elapsedMs: Date.now() - started,
363
- ...attach ? { attach } : {},
364
- ...workspace ? { workspace } : {}
365
- });
366
- } catch (e) {
367
- await Promise.resolve(
368
- pi.sendMessage(
369
- {
370
- customType: DELEGATE_MESSAGE_TYPE,
371
- content: `Delegate ${W}/${name} settled, but delivering its answer failed: ${e instanceof Error ? e.message : String(e)}. Read it with: mu agent read ${name} -n 50 -w ${W}`,
372
- display: true,
373
- details: { name, workstream: W, outcome: null, closed: false }
374
- },
375
- { deliverAs: "followUp", triggerTurn: true }
376
- )
377
- ).catch(() => {
378
- });
379
- }
380
- };
381
- void waiting.then(async (r) => {
382
- if (inflight.get(name) !== entry) return;
383
- if (entry.cancelling) entry.parked = () => settle(r);
384
- else await settle(r);
385
- });
386
- const lines = [
387
- `Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
388
- workspace ? `Workspace: ${workspace}` : void 0,
389
- attach ? `Attach: ${attach}` : void 0,
390
- `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
391
- ].filter((x) => x !== void 0);
392
- return {
393
- content: [{ type: "text", text: lines.join("\n") }],
394
- details: {
395
- name,
396
- workstream: W,
397
- attach: attach ?? null,
398
- workspace: workspace ?? null,
399
- keep
484
+ const handle = `queued-${++queueSeq}`;
485
+ queue.push({
486
+ handle,
487
+ params: {
488
+ ...params,
489
+ cwd: typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd()
490
+ },
491
+ ctx
492
+ });
493
+ showStatus();
494
+ return {
495
+ content: [
496
+ {
497
+ type: "text",
498
+ text: `Queued as ${handle} (#${queue.length}; ${inflight.size + starting} running, MU_DELEGATE_MAX=${max}). It starts when a slot frees, and its answer arrives as a follow-up message like any delegate's; do not wait or poll. Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${handle}" }`
400
499
  }
401
- };
402
- } finally {
403
- freeSlot();
404
- }
500
+ ],
501
+ details: { name: handle, workstream: W, queued: true, position: queue.length }
502
+ };
405
503
  }
406
504
  });
407
505
  pi.registerTool({
@@ -421,6 +519,15 @@ ${task}` : task,
421
519
  track(ctx);
422
520
  const name = typeof params.name === "string" ? params.name : "";
423
521
  if (!name) throw new Error("mu_delegate_cancel needs a name");
522
+ const qi = queue.findIndex((q) => q.handle === name);
523
+ if (qi >= 0) {
524
+ queue.splice(qi, 1);
525
+ showStatus();
526
+ return {
527
+ content: [{ type: "text", text: `Dropped ${name} from the queue; it never started.` }],
528
+ details: { name, workstream: W }
529
+ };
530
+ }
424
531
  const entry = inflight.get(name);
425
532
  if (entry) entry.cancelling = true;
426
533
  const ab = await mu(["agent", "abort", name, "-w", W]);
@@ -437,6 +544,7 @@ ${task}` : task,
437
544
  entry.abort.abort();
438
545
  forget(name);
439
546
  }
547
+ failed.delete(name);
440
548
  let text = `Aborted ${W}/${name}.`;
441
549
  if (params.keep === true) text += " Pane kept.";
442
550
  else {
@@ -447,13 +555,21 @@ ${task}` : task,
447
555
  }
448
556
  });
449
557
  pi.on("session_shutdown", (_e, ctx) => {
450
- if (inflight.size === 0) return;
558
+ if (inflight.size === 0 && queue.length === 0) return;
451
559
  const names = [...inflight.keys()];
560
+ const never = queue.splice(0).map((q) => q.handle);
452
561
  for (const e of inflight.values()) e.abort.abort();
453
562
  inflight.clear();
454
563
  showStatus();
455
564
  ui = void 0;
456
- const msg = `${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`;
565
+ const parts = [];
566
+ if (names.length > 0)
567
+ parts.push(
568
+ `${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`
569
+ );
570
+ if (never.length > 0)
571
+ parts.push(`${never.length} queued, never started: ${never.join(", ")}. Re-issue them.`);
572
+ const msg = parts.join(" ");
457
573
  if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
458
574
  else process.stderr.write(`mu: ${msg}
459
575
  `);
@@ -465,6 +581,7 @@ import { existsSync as existsSync2, readFileSync } from "fs";
465
581
  import { join as join2 } from "path";
466
582
  var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
467
583
  var NUDGE_LOG_KIND = "nudge";
584
+ var CLOSE_NUDGE_MESSAGE_TYPE = "mu-close-task";
468
585
  var MARK_START = "<!-- mu:keep-driving -->";
469
586
  var MARK_END = "<!-- /mu:keep-driving -->";
470
587
  var WAIT_HINT_MAX = 8;
@@ -588,9 +705,71 @@ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
588
705
  };
589
706
  });
590
707
  }
708
+ function workerIdentity(env = process.env) {
709
+ const agent = env.MU_AGENT_NAME;
710
+ const workstream = env.MU_WORKSTREAM;
711
+ if (!agent || !workstream || workstream === "scratch") return void 0;
712
+ return { agent, workstream };
713
+ }
714
+ async function ownedInProgress(run, agent, workstream) {
715
+ const r = await run(["task", "owned-by", agent, "-w", workstream, "--json"]);
716
+ if (r.code !== 0) return void 0;
717
+ try {
718
+ const json = JSON.parse(r.stdout);
719
+ return (json.items ?? []).flatMap(
720
+ (t) => t.status === "IN_PROGRESS" && t.name ? [t.name] : []
721
+ );
722
+ } catch {
723
+ return void 0;
724
+ }
725
+ }
726
+ function closeNudgeText(workstream, names) {
727
+ const close = names.map((n) => `mu task close ${n} -w ${workstream} --evidence "<what you verified>"`).join("\n ");
728
+ return [
729
+ `[mu] You are stopping while you still own ${names.map((n) => `${workstream}/${n}`).join(", ")} (IN_PROGRESS).`,
730
+ "If the work is done and verified, close it now:",
731
+ ` ${close}`,
732
+ "If you are blocked or need an answer, say so in one line and stop; the orchestrator will read your pane."
733
+ ].join("\n");
734
+ }
735
+ function registerCloseNudge(pi, run = defaultRunner(), env = process.env) {
736
+ if (!nudgeEnabled(env)) return;
737
+ const me = workerIdentity(env);
738
+ if (me === void 0) return;
739
+ let fired = false;
740
+ pi.on("input", () => {
741
+ fired = false;
742
+ });
743
+ pi.on("agent_before_settle", async (event) => {
744
+ if (fired) return;
745
+ if (outcomeOf(event) !== "completed") return;
746
+ fired = true;
747
+ const names = await ownedInProgress(run, me.agent, me.workstream);
748
+ if (names === void 0 || names.length === 0) return;
749
+ await run([
750
+ "log",
751
+ "-w",
752
+ me.workstream,
753
+ "--kind",
754
+ NUDGE_LOG_KIND,
755
+ `close: ${me.agent} settled owning ${names.join(", ")}`
756
+ ]);
757
+ return {
758
+ entries: [
759
+ {
760
+ type: "custom_message",
761
+ customType: CLOSE_NUDGE_MESSAGE_TYPE,
762
+ content: closeNudgeText(me.workstream, names),
763
+ display: true
764
+ }
765
+ ],
766
+ continue: true
767
+ };
768
+ });
769
+ }
591
770
 
592
771
  // extension/mu-pi.ts
593
- var EXT_VERSION = true ? "3.4.0" : void 0;
772
+ var EXT_VERSION = true ? "3.6.0" : void 0;
594
773
  var FRESH_COMMAND = "mu-fresh";
595
774
  var FRESH_TIMEOUT_MS = 3e4;
596
775
  function commandName(name) {
@@ -661,6 +840,19 @@ function lastAssistantText(event) {
661
840
  }
662
841
  return "";
663
842
  }
843
+ function lastAssistantError(event) {
844
+ const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
845
+ if (!Array.isArray(msgs)) return void 0;
846
+ for (let i = msgs.length - 1; i >= 0; i--) {
847
+ const m = msgs[i];
848
+ if (typeof m !== "object" || m === null) continue;
849
+ const { role, stopReason, errorMessage } = m;
850
+ if (role !== "assistant") continue;
851
+ if (stopReason !== "error") return void 0;
852
+ return typeof errorMessage === "string" && errorMessage !== "" ? capText(errorMessage) : "unknown error";
853
+ }
854
+ return void 0;
855
+ }
664
856
  function capText(text) {
665
857
  if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return text;
666
858
  const cut = Buffer.from(text, "utf8").subarray(0, LAST_TEXT_MAX_BYTES).toString("utf8");
@@ -720,6 +912,7 @@ function muPi(pi) {
720
912
  registerDelegate(pi);
721
913
  registerPrompts(pi);
722
914
  registerNudge(pi);
915
+ registerCloseNudge(pi);
723
916
  serveCtl(pi);
724
917
  }
725
918
  function serveCtl(pi) {
@@ -768,6 +961,7 @@ function serveCtl(pi) {
768
961
  });
769
962
  pi.on("agent_end", (e) => {
770
963
  g.endText = lastAssistantText(e);
964
+ g.endError = lastAssistantError(e);
771
965
  });
772
966
  pi.on("agent_settled", (_e, c) => {
773
967
  g.ctx = c;
@@ -776,6 +970,8 @@ function serveCtl(pi) {
776
970
  g.runs++;
777
971
  g.lastText = g.endText ?? "";
778
972
  g.endText = void 0;
973
+ g.lastError = g.endError;
974
+ g.endError = void 0;
779
975
  for (const w of [...g.waiters]) w();
780
976
  });
781
977
  pi.registerCommand(FRESH_COMMAND, {
@@ -914,9 +1110,13 @@ function serveCtl(pi) {
914
1110
  return { ...fail(`${UNKNOWN_OP_PREFIX}${String(req.op)}`), ops: [...CTL_OPS] };
915
1111
  }
916
1112
  }
1113
+ const settledRun = () => ({
1114
+ lastText: g.lastText,
1115
+ ...g.lastError !== void 0 ? { lastError: g.lastError } : {}
1116
+ });
917
1117
  function wait(afterRuns, timeoutMs, conn) {
918
1118
  if (afterRuns !== void 0 && g.runs > afterRuns && g.state === "idle") {
919
- return Promise.resolve({ v: V, ok: true, ...status(), lastText: g.lastText });
1119
+ return Promise.resolve({ v: V, ok: true, ...status(), ...settledRun() });
920
1120
  }
921
1121
  return new Promise((resolve) => {
922
1122
  let timer;
@@ -926,7 +1126,7 @@ function serveCtl(pi) {
926
1126
  conn.off("close", onClose);
927
1127
  resolve(r);
928
1128
  };
929
- const waiter = () => done({ v: V, ok: true, ...status(), lastText: g.lastText });
1129
+ const waiter = () => done({ v: V, ok: true, ...status(), ...settledRun() });
930
1130
  const onClose = () => done(fail("client closed"));
931
1131
  g.waiters.add(waiter);
932
1132
  conn.on("close", onClose);
@@ -1041,6 +1241,7 @@ export {
1041
1241
  LIVE_PROBE_MS,
1042
1242
  commandName,
1043
1243
  muPi as default,
1244
+ lastAssistantError,
1044
1245
  lastAssistantText,
1045
1246
  promptsDir
1046
1247
  };