@mu-crew/mu 3.4.0 → 3.5.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
  `);
@@ -590,7 +706,7 @@ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
590
706
  }
591
707
 
592
708
  // extension/mu-pi.ts
593
- var EXT_VERSION = true ? "3.4.0" : void 0;
709
+ var EXT_VERSION = true ? "3.5.0" : void 0;
594
710
  var FRESH_COMMAND = "mu-fresh";
595
711
  var FRESH_TIMEOUT_MS = 3e4;
596
712
  function commandName(name) {
@@ -661,6 +777,19 @@ function lastAssistantText(event) {
661
777
  }
662
778
  return "";
663
779
  }
780
+ function lastAssistantError(event) {
781
+ const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
782
+ if (!Array.isArray(msgs)) return void 0;
783
+ for (let i = msgs.length - 1; i >= 0; i--) {
784
+ const m = msgs[i];
785
+ if (typeof m !== "object" || m === null) continue;
786
+ const { role, stopReason, errorMessage } = m;
787
+ if (role !== "assistant") continue;
788
+ if (stopReason !== "error") return void 0;
789
+ return typeof errorMessage === "string" && errorMessage !== "" ? capText(errorMessage) : "unknown error";
790
+ }
791
+ return void 0;
792
+ }
664
793
  function capText(text) {
665
794
  if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return text;
666
795
  const cut = Buffer.from(text, "utf8").subarray(0, LAST_TEXT_MAX_BYTES).toString("utf8");
@@ -768,6 +897,7 @@ function serveCtl(pi) {
768
897
  });
769
898
  pi.on("agent_end", (e) => {
770
899
  g.endText = lastAssistantText(e);
900
+ g.endError = lastAssistantError(e);
771
901
  });
772
902
  pi.on("agent_settled", (_e, c) => {
773
903
  g.ctx = c;
@@ -776,6 +906,8 @@ function serveCtl(pi) {
776
906
  g.runs++;
777
907
  g.lastText = g.endText ?? "";
778
908
  g.endText = void 0;
909
+ g.lastError = g.endError;
910
+ g.endError = void 0;
779
911
  for (const w of [...g.waiters]) w();
780
912
  });
781
913
  pi.registerCommand(FRESH_COMMAND, {
@@ -914,9 +1046,13 @@ function serveCtl(pi) {
914
1046
  return { ...fail(`${UNKNOWN_OP_PREFIX}${String(req.op)}`), ops: [...CTL_OPS] };
915
1047
  }
916
1048
  }
1049
+ const settledRun = () => ({
1050
+ lastText: g.lastText,
1051
+ ...g.lastError !== void 0 ? { lastError: g.lastError } : {}
1052
+ });
917
1053
  function wait(afterRuns, timeoutMs, conn) {
918
1054
  if (afterRuns !== void 0 && g.runs > afterRuns && g.state === "idle") {
919
- return Promise.resolve({ v: V, ok: true, ...status(), lastText: g.lastText });
1055
+ return Promise.resolve({ v: V, ok: true, ...status(), ...settledRun() });
920
1056
  }
921
1057
  return new Promise((resolve) => {
922
1058
  let timer;
@@ -926,7 +1062,7 @@ function serveCtl(pi) {
926
1062
  conn.off("close", onClose);
927
1063
  resolve(r);
928
1064
  };
929
- const waiter = () => done({ v: V, ok: true, ...status(), lastText: g.lastText });
1065
+ const waiter = () => done({ v: V, ok: true, ...status(), ...settledRun() });
930
1066
  const onClose = () => done(fail("client closed"));
931
1067
  g.waiters.add(waiter);
932
1068
  conn.on("close", onClose);
@@ -1041,6 +1177,7 @@ export {
1041
1177
  LIVE_PROBE_MS,
1042
1178
  commandName,
1043
1179
  muPi as default,
1180
+ lastAssistantError,
1044
1181
  lastAssistantText,
1045
1182
  promptsDir
1046
1183
  };