@mu-crew/mu 3.3.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.
Files changed (45) hide show
  1. package/README.md +45 -31
  2. package/dist/cli.js +812 -429
  3. package/dist/cli.js.map +1 -1
  4. package/dist/extension/mu-pi.js +221 -55
  5. package/dist/extension/mu-pi.js.map +1 -1
  6. package/dist/index.d.ts +33 -4
  7. package/dist/index.js +76 -5
  8. package/dist/index.js.map +1 -1
  9. package/docs/VISION.md +5 -1
  10. package/docs/VOCABULARY.md +4 -1
  11. package/docs/architecture/dag.md +3 -3
  12. package/docs/architecture/ops-log.md +26 -7
  13. package/docs/guide/cleanup.md +19 -0
  14. package/docs/guide/delegate.md +43 -4
  15. package/docs/reference/env.md +1 -0
  16. package/docs/reference/naming.md +18 -0
  17. package/docs/specs/2026-09-29-task-substates.md +1 -1
  18. package/package.json +1 -1
  19. package/prompts/mu-debug.md +8 -0
  20. package/prompts/mu-sweep.md +8 -0
  21. package/prompts/mu-until.md +8 -0
  22. package/prompts/ultrathink.md +2 -1
  23. package/skills/mu/SKILL.md +70 -31
  24. package/skills/mu/recipes/adversarial-review.md +27 -19
  25. package/skills/mu/recipes/{triage.md → backlog-triage.md} +9 -4
  26. package/skills/mu/recipes/codemode-driver.md +68 -20
  27. package/skills/mu/recipes/deep-research.md +23 -8
  28. package/skills/mu/recipes/fan-out.md +15 -11
  29. package/skills/mu/recipes/findings.md +103 -0
  30. package/skills/mu/recipes/hypothesis-panel.md +18 -13
  31. package/skills/mu/recipes/long-run.md +4 -4
  32. package/skills/mu/recipes/loop-until-done.md +7 -3
  33. package/skills/mu/recipes/orchestrator-loop.md +44 -27
  34. package/skills/mu/recipes/plan.md +8 -3
  35. package/skills/mu/recipes/recovery.md +5 -0
  36. package/skills/mu/recipes/refute.md +38 -36
  37. package/skills/mu/recipes/remote-workers.md +7 -2
  38. package/skills/mu/recipes/review-panel.md +36 -31
  39. package/skills/mu/recipes/rules-audit.md +30 -17
  40. package/skills/mu/recipes/tasks-or-calls.md +71 -0
  41. package/skills/mu/recipes/tournament.md +13 -10
  42. package/skills/mu/recipes/ultrathink.md +20 -11
  43. package/skills/mu/recipes/watcher.md +4 -1
  44. package/skills/mu/recipes/waves.md +6 -5
  45. package/skills/mu/recipes/worker.md +3 -1
@@ -51,10 +51,22 @@ 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;
64
+ var DELEGATE_MAX_DEFAULT = 16;
65
+ var DELEGATE_QUEUE_FACTOR = 4;
66
+ function delegateMax(env = process.env) {
67
+ const n = Number(env.MU_DELEGATE_MAX);
68
+ return Number.isInteger(n) && n > 0 ? n : DELEGATE_MAX_DEFAULT;
69
+ }
58
70
  var W = DELEGATE_WORKSTREAM;
59
71
  function delegateEnabled(env = process.env) {
60
72
  if (env.MU_MANAGED_AGENT) return false;
@@ -101,7 +113,7 @@ function formatElapsed(ms) {
101
113
  }
102
114
  function delegateMessage(name, agent, pane, x = {}) {
103
115
  const timeoutS = x.timeoutS ?? DELEGATE_TIMEOUT_S;
104
- const who = `Delegate ${W}/${name}`;
116
+ const who = `Delegate ${W}/${name}${x.queuedAs ? ` (queued as ${x.queuedAs})` : ""}`;
105
117
  const took = x.elapsedMs !== void 0 ? ` after ${formatElapsed(x.elapsedMs)}` : "";
106
118
  const fate = pane.closed ? "Pane closed." : `Pane kept (${pane.why}).`;
107
119
  const look = [
@@ -120,6 +132,13 @@ ${x.tail.trimEnd()}` : "";
120
132
  ${agent.lastText ?? ""}`;
121
133
  case "empty":
122
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");
123
142
  case "died":
124
143
  return `${who} died${took} before answering. ${fate}${ws}${tailBlock}`;
125
144
  case "timeout":
@@ -141,11 +160,29 @@ function labelStem(label) {
141
160
  function registerDelegate(pi, run = defaultRunner()) {
142
161
  if (!delegateEnabled()) return;
143
162
  const inflight = /* @__PURE__ */ new Map();
163
+ let starting = 0;
164
+ const queue = [];
165
+ let queueSeq = 0;
166
+ const failed = /* @__PURE__ */ new Set();
144
167
  const reserved = /* @__PURE__ */ new Set();
145
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
+ };
146
179
  const showStatus = () => {
180
+ reportPending();
147
181
  try {
148
- 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
+ );
149
186
  } catch {
150
187
  }
151
188
  };
@@ -155,8 +192,34 @@ function registerDelegate(pi, run = defaultRunner()) {
155
192
  const forget = (name) => {
156
193
  inflight.delete(name);
157
194
  reserved.delete(name);
195
+ drain();
158
196
  showStatus();
159
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
+ }
160
223
  const mu = run;
161
224
  const json = (r) => {
162
225
  try {
@@ -173,6 +236,10 @@ function registerDelegate(pi, run = defaultRunner()) {
173
236
  const taken = new Set(
174
237
  Array.isArray(agents) ? agents.map((a) => a.name).filter((n) => n) : []
175
238
  );
239
+ if (Array.isArray(agents)) {
240
+ for (const f of [...failed]) if (!taken.has(f)) failed.delete(f);
241
+ showStatus();
242
+ }
176
243
  for (let n = 1; ; n++) {
177
244
  const name = stem ? n === 1 ? `delegate-${stem}` : `delegate-${stem}-${n}` : `delegate-${n}`;
178
245
  if (!taken.has(name) && !reserved.has(name)) {
@@ -224,7 +291,7 @@ function registerDelegate(pi, run = defaultRunner()) {
224
291
  async function deliver(name, wait, keep, extras) {
225
292
  const agent = json(wait)?.agents?.[0];
226
293
  const outcome = agent?.outcome;
227
- 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;
228
295
  let pane = { closed: false, why: "keep: true" };
229
296
  if (outcome !== "done" && outcome !== "empty")
230
297
  pane = { closed: false, why: "kept as evidence" };
@@ -232,6 +299,10 @@ function registerDelegate(pi, run = defaultRunner()) {
232
299
  const c = await mu(["agent", "close", name, "-w", W]);
233
300
  pane = c.code === 0 ? { closed: true } : { closed: false, why: `close refused: ${(c.stderr || c.stdout).trim()}` };
234
301
  }
302
+ if (outcome === "error" && !pane.closed) {
303
+ failed.add(name);
304
+ showStatus();
305
+ }
235
306
  await pi.sendMessage(
236
307
  {
237
308
  customType: DELEGATE_MESSAGE_TYPE,
@@ -250,52 +321,18 @@ function registerDelegate(pi, run = defaultRunner()) {
250
321
  { deliverAs: "followUp", triggerTurn: true }
251
322
  );
252
323
  }
253
- pi.registerTool({
254
- name: DELEGATE_TOOL,
255
- label: "mu delegate",
256
- 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. 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.`,
257
- promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
258
- parameters: {
259
- type: "object",
260
- properties: {
261
- task: { type: "string", description: "The complete task, with all context it needs" },
262
- label: {
263
- type: "string",
264
- description: "Short label naming the delegate, e.g. 'review' \u2192 delegate-review. Use one per subtask when fanning out."
265
- },
266
- brief: {
267
- type: "string",
268
- description: "Optional standing role or rules, sent before the task, e.g. 'You are a reviewer; read only.' The rules are instructions, not enforced. The job itself goes in task."
269
- },
270
- cwd: {
271
- type: "string",
272
- description: "Working directory for the delegate (default: yours; ignored with workspace)"
273
- },
274
- timeout: {
275
- type: "number",
276
- exclusiveMinimum: 0,
277
- description: `Seconds to wait for the answer (default ${DELEGATE_TIMEOUT_S}). On timeout you get a "still running" follow-up instead of the answer, and the answer will not arrive later; the subagent keeps running in its pane.`
278
- },
279
- workspace: {
280
- type: "boolean",
281
- description: "For tasks that edit files: its own VCS checkout (jj workspace / git worktree / sl share) branched from the current commit. The result and follow-up name the path; changes stay there until you merge them. This isolates repository edits only: the subagent runs as you, with your files, environment and network."
282
- },
283
- cli: {
284
- type: "string",
285
- description: "Agent CLI key (default pi). Name a configured key such as 'pi_fast' to run a cheaper model."
286
- },
287
- keep: {
288
- type: "boolean",
289
- description: "Keep the pane after it finishes, to talk to it again"
290
- }
291
- },
292
- required: ["task"],
293
- additionalProperties: false
294
- },
295
- async execute(_id, params, signal, _onUpdate, ctx) {
296
- track(ctx);
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 {
297
335
  const task = typeof params.task === "string" ? params.task.trim() : "";
298
- if (!task) throw new Error("mu_delegate needs a non-empty task");
299
336
  const brief = typeof params.brief === "string" ? params.brief.trim() : "";
300
337
  const keep = params.keep === true;
301
338
  const t = params.timeout;
@@ -312,6 +349,7 @@ function registerDelegate(pi, run = defaultRunner()) {
312
349
  const { name, attach, workspace } = await spawn(params, cwd, signal);
313
350
  const entry = { abort: new AbortController(), cancelling: false };
314
351
  inflight.set(name, entry);
352
+ freeSlot();
315
353
  showStatus();
316
354
  const started = Date.now();
317
355
  const waiting = mu(
@@ -334,6 +372,7 @@ ${task}` : task, "-w", W]);
334
372
  await deliver(name, r, keep, {
335
373
  timeoutS,
336
374
  elapsedMs: Date.now() - started,
375
+ ...queuedAs ? { queuedAs } : {},
337
376
  ...attach ? { attach } : {},
338
377
  ...workspace ? { workspace } : {}
339
378
  });
@@ -373,6 +412,94 @@ ${task}` : task, "-w", W]);
373
412
  keep
374
413
  }
375
414
  };
415
+ } finally {
416
+ freeSlot();
417
+ if (queuedAs) drain();
418
+ }
419
+ }
420
+ pi.registerTool({
421
+ name: DELEGATE_TOOL,
422
+ label: "mu delegate",
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.`,
424
+ promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
425
+ parameters: {
426
+ type: "object",
427
+ properties: {
428
+ task: { type: "string", description: "The complete task, with all context it needs" },
429
+ label: {
430
+ type: "string",
431
+ description: "Short label naming the delegate, e.g. 'review' \u2192 delegate-review. Use one per subtask when fanning out."
432
+ },
433
+ brief: {
434
+ type: "string",
435
+ description: "Optional standing role or rules, sent before the task, e.g. 'You are a reviewer; read only.' The rules are instructions, not enforced. The job itself goes in task."
436
+ },
437
+ cwd: {
438
+ type: "string",
439
+ description: "Working directory for the delegate (default: yours; ignored with workspace)"
440
+ },
441
+ timeout: {
442
+ type: "number",
443
+ exclusiveMinimum: 0,
444
+ description: `Seconds to wait for the answer (default ${DELEGATE_TIMEOUT_S}). On timeout you get a "still running" follow-up instead of the answer, and the answer will not arrive later; the subagent keeps running in its pane.`
445
+ },
446
+ workspace: {
447
+ type: "boolean",
448
+ description: "For tasks that edit files: its own VCS checkout (jj workspace / git worktree / sl share) branched from the current commit. The result and follow-up name the path; changes stay there until you merge them. This isolates repository edits only: the subagent runs as you, with your files, environment and network."
449
+ },
450
+ cli: {
451
+ type: "string",
452
+ description: "Agent CLI key (default pi). Name a configured key such as 'pi_fast' to run a cheaper model."
453
+ },
454
+ keep: {
455
+ type: "boolean",
456
+ description: "Keep the pane after it finishes, to talk to it again"
457
+ }
458
+ },
459
+ required: ["task"],
460
+ additionalProperties: false
461
+ },
462
+ async execute(_id, params, signal, _onUpdate, ctx) {
463
+ track(ctx);
464
+ const task = typeof params.task === "string" ? params.task.trim() : "";
465
+ if (!task) throw new Error("mu_delegate needs a non-empty task");
466
+ const t = params.timeout;
467
+ if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
468
+ throw new Error(
469
+ `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
470
+ );
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).`
483
+ );
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}" }`
499
+ }
500
+ ],
501
+ details: { name: handle, workstream: W, queued: true, position: queue.length }
502
+ };
376
503
  }
377
504
  });
378
505
  pi.registerTool({
@@ -392,6 +519,15 @@ ${task}` : task, "-w", W]);
392
519
  track(ctx);
393
520
  const name = typeof params.name === "string" ? params.name : "";
394
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
+ }
395
531
  const entry = inflight.get(name);
396
532
  if (entry) entry.cancelling = true;
397
533
  const ab = await mu(["agent", "abort", name, "-w", W]);
@@ -408,6 +544,7 @@ ${task}` : task, "-w", W]);
408
544
  entry.abort.abort();
409
545
  forget(name);
410
546
  }
547
+ failed.delete(name);
411
548
  let text = `Aborted ${W}/${name}.`;
412
549
  if (params.keep === true) text += " Pane kept.";
413
550
  else {
@@ -418,13 +555,21 @@ ${task}` : task, "-w", W]);
418
555
  }
419
556
  });
420
557
  pi.on("session_shutdown", (_e, ctx) => {
421
- if (inflight.size === 0) return;
558
+ if (inflight.size === 0 && queue.length === 0) return;
422
559
  const names = [...inflight.keys()];
560
+ const never = queue.splice(0).map((q) => q.handle);
423
561
  for (const e of inflight.values()) e.abort.abort();
424
562
  inflight.clear();
425
563
  showStatus();
426
564
  ui = void 0;
427
- 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(" ");
428
573
  if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
429
574
  else process.stderr.write(`mu: ${msg}
430
575
  `);
@@ -561,7 +706,7 @@ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
561
706
  }
562
707
 
563
708
  // extension/mu-pi.ts
564
- var EXT_VERSION = true ? "3.3.0" : void 0;
709
+ var EXT_VERSION = true ? "3.5.0" : void 0;
565
710
  var FRESH_COMMAND = "mu-fresh";
566
711
  var FRESH_TIMEOUT_MS = 3e4;
567
712
  function commandName(name) {
@@ -632,6 +777,19 @@ function lastAssistantText(event) {
632
777
  }
633
778
  return "";
634
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
+ }
635
793
  function capText(text) {
636
794
  if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return text;
637
795
  const cut = Buffer.from(text, "utf8").subarray(0, LAST_TEXT_MAX_BYTES).toString("utf8");
@@ -739,6 +897,7 @@ function serveCtl(pi) {
739
897
  });
740
898
  pi.on("agent_end", (e) => {
741
899
  g.endText = lastAssistantText(e);
900
+ g.endError = lastAssistantError(e);
742
901
  });
743
902
  pi.on("agent_settled", (_e, c) => {
744
903
  g.ctx = c;
@@ -747,6 +906,8 @@ function serveCtl(pi) {
747
906
  g.runs++;
748
907
  g.lastText = g.endText ?? "";
749
908
  g.endText = void 0;
909
+ g.lastError = g.endError;
910
+ g.endError = void 0;
750
911
  for (const w of [...g.waiters]) w();
751
912
  });
752
913
  pi.registerCommand(FRESH_COMMAND, {
@@ -885,9 +1046,13 @@ function serveCtl(pi) {
885
1046
  return { ...fail(`${UNKNOWN_OP_PREFIX}${String(req.op)}`), ops: [...CTL_OPS] };
886
1047
  }
887
1048
  }
1049
+ const settledRun = () => ({
1050
+ lastText: g.lastText,
1051
+ ...g.lastError !== void 0 ? { lastError: g.lastError } : {}
1052
+ });
888
1053
  function wait(afterRuns, timeoutMs, conn) {
889
1054
  if (afterRuns !== void 0 && g.runs > afterRuns && g.state === "idle") {
890
- return Promise.resolve({ v: V, ok: true, ...status(), lastText: g.lastText });
1055
+ return Promise.resolve({ v: V, ok: true, ...status(), ...settledRun() });
891
1056
  }
892
1057
  return new Promise((resolve) => {
893
1058
  let timer;
@@ -897,7 +1062,7 @@ function serveCtl(pi) {
897
1062
  conn.off("close", onClose);
898
1063
  resolve(r);
899
1064
  };
900
- const waiter = () => done({ v: V, ok: true, ...status(), lastText: g.lastText });
1065
+ const waiter = () => done({ v: V, ok: true, ...status(), ...settledRun() });
901
1066
  const onClose = () => done(fail("client closed"));
902
1067
  g.waiters.add(waiter);
903
1068
  conn.on("close", onClose);
@@ -1012,6 +1177,7 @@ export {
1012
1177
  LIVE_PROBE_MS,
1013
1178
  commandName,
1014
1179
  muPi as default,
1180
+ lastAssistantError,
1015
1181
  lastAssistantText,
1016
1182
  promptsDir
1017
1183
  };