@mu-crew/mu 3.13.3 → 4.0.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 (119) hide show
  1. package/AGENTS.md +73 -35
  2. package/README.md +71 -57
  3. package/dist/chunk-5HECDBK3.js +4482 -0
  4. package/dist/chunk-5HECDBK3.js.map +1 -0
  5. package/dist/chunk-AW2NONYU.js +301 -0
  6. package/dist/chunk-AW2NONYU.js.map +1 -0
  7. package/dist/chunk-GQNHIPJN.js +4060 -0
  8. package/dist/chunk-GQNHIPJN.js.map +1 -0
  9. package/dist/{chunk-3BARPJLY.js → chunk-PZ5GOCWH.js} +16 -2
  10. package/dist/chunk-PZ5GOCWH.js.map +1 -0
  11. package/dist/chunk-S424D5EP.js +13766 -0
  12. package/dist/chunk-S424D5EP.js.map +1 -0
  13. package/dist/{cli-CKOMHBXV.js → cli-GB2CAVVW.js} +6776 -4350
  14. package/dist/cli-GB2CAVVW.js.map +1 -0
  15. package/dist/cli.js +2 -3
  16. package/dist/cli.js.map +1 -1
  17. package/dist/errors-KHOYKD7D.js +65 -0
  18. package/dist/extension/mu-pi.js +1617 -836
  19. package/dist/extension/mu-pi.js.map +1 -1
  20. package/dist/index.d.ts +4015 -2561
  21. package/dist/index.js +614 -297
  22. package/dist/index.js.map +1 -1
  23. package/dist/tui-ALPHYTA2.js +1773 -0
  24. package/dist/tui-ALPHYTA2.js.map +1 -0
  25. package/docs/ARCHITECTURE.md +117 -66
  26. package/docs/VOCABULARY.md +112 -49
  27. package/docs/guide/README.md +3 -3
  28. package/docs/guide/backends.md +8 -4
  29. package/docs/guide/cleanup.md +61 -33
  30. package/docs/guide/delegate.md +141 -66
  31. package/docs/guide/dispatch.md +148 -53
  32. package/docs/guide/getting-started.md +37 -44
  33. package/docs/guide/other-agent-clis.md +102 -0
  34. package/docs/guide/recovery.md +31 -50
  35. package/docs/guide/remote.md +115 -45
  36. package/docs/guide/sql.md +104 -24
  37. package/docs/guide/stop-a-worker.md +30 -37
  38. package/docs/guide/sync.md +11 -3
  39. package/docs/guide/upgrade.md +93 -43
  40. package/docs/reference/env.md +23 -32
  41. package/docs/reference/naming.md +12 -7
  42. package/docs/reference/surface-contract.md +153 -0
  43. package/package.json +33 -11
  44. package/prompts/mu-debug.md +3 -1
  45. package/prompts/mu-refute.md +2 -1
  46. package/prompts/mu-research.md +3 -1
  47. package/prompts/mu-review.md +2 -1
  48. package/prompts/mu-rules-audit.md +5 -4
  49. package/prompts/mu-sweep.md +2 -1
  50. package/prompts/mu-tournament.md +4 -2
  51. package/prompts/mu-until.md +3 -1
  52. package/prompts/ultrathink.md +1 -1
  53. package/skills/mu/SKILL.md +60 -291
  54. package/skills/mu/recipes/adversarial-review.md +12 -103
  55. package/skills/mu/recipes/brief.md +14 -61
  56. package/skills/mu/recipes/course-correct.md +23 -0
  57. package/skills/mu/recipes/findings.md +17 -111
  58. package/skills/mu/recipes/models.md +17 -92
  59. package/skills/mu/recipes/move.md +14 -0
  60. package/skills/mu/recipes/orchestrator-loop.md +20 -119
  61. package/skills/mu/recipes/recovery.md +8 -29
  62. package/skills/mu/recipes/remote-workers.md +16 -318
  63. package/skills/mu/recipes/tasks-or-calls.md +15 -78
  64. package/skills/mu/recipes/test-health.md +19 -77
  65. package/skills/mu/recipes/worker.md +17 -24
  66. package/dist/chunk-45KQLMEL.js +0 -2908
  67. package/dist/chunk-45KQLMEL.js.map +0 -1
  68. package/dist/chunk-G434JKV2.js +0 -641
  69. package/dist/chunk-G434JKV2.js.map +0 -1
  70. package/dist/chunk-KUGUFBWH.js +0 -12527
  71. package/dist/chunk-KUGUFBWH.js.map +0 -1
  72. package/dist/chunk-QITLNZEM.js +0 -87
  73. package/dist/chunk-QITLNZEM.js.map +0 -1
  74. package/dist/chunk-VIEYQMQA.js +0 -393
  75. package/dist/chunk-VIEYQMQA.js.map +0 -1
  76. package/dist/chunk-WI6AWCMK.js +0 -19
  77. package/dist/chunk-WI6AWCMK.js.map +0 -1
  78. package/dist/cli-CKOMHBXV.js.map +0 -1
  79. package/dist/devtools-LS4ZEPWM.js +0 -3772
  80. package/dist/devtools-LS4ZEPWM.js.map +0 -1
  81. package/dist/tui-LFJ5P4Y2.js +0 -40956
  82. package/dist/tui-LFJ5P4Y2.js.map +0 -1
  83. package/dist/tui-sync-worker.js +0 -17
  84. package/dist/tui-sync-worker.js.map +0 -1
  85. package/dist/yank-XDOONQSL.js +0 -13
  86. package/dist/yank-XDOONQSL.js.map +0 -1
  87. package/docs/ROADMAP.md +0 -123
  88. package/docs/VISION.md +0 -298
  89. package/docs/architecture/control-socket.md +0 -142
  90. package/docs/architecture/dag.md +0 -117
  91. package/docs/architecture/mux.md +0 -167
  92. package/docs/architecture/ops-log.md +0 -160
  93. package/docs/architecture/sdk.md +0 -187
  94. package/docs/architecture/sync.md +0 -159
  95. package/docs/architecture/tui.md +0 -165
  96. package/docs/guide/tui.md +0 -98
  97. package/docs/history/CHANGELOG-pre-3.md +0 -4866
  98. package/docs/img/tui-dashboard.png +0 -0
  99. package/docs/specs/2026-09-28-agent-state-from-murmur.md +0 -333
  100. package/docs/specs/2026-09-29-task-substates.md +0 -247
  101. package/docs/specs/2026-10-03-keep-driving-nudge.md +0 -163
  102. package/docs/specs/2026-10-21-fast-and-slow-gates.md +0 -229
  103. package/skills/mu/recipes/backlog-triage.md +0 -45
  104. package/skills/mu/recipes/codemode-driver.md +0 -118
  105. package/skills/mu/recipes/deep-research.md +0 -69
  106. package/skills/mu/recipes/drift-audit.md +0 -75
  107. package/skills/mu/recipes/fan-out.md +0 -42
  108. package/skills/mu/recipes/hypothesis-panel.md +0 -48
  109. package/skills/mu/recipes/long-run.md +0 -32
  110. package/skills/mu/recipes/loop-until-done.md +0 -45
  111. package/skills/mu/recipes/plan.md +0 -67
  112. package/skills/mu/recipes/refute.md +0 -55
  113. package/skills/mu/recipes/review-panel.md +0 -65
  114. package/skills/mu/recipes/rules-audit.md +0 -89
  115. package/skills/mu/recipes/tournament.md +0 -44
  116. package/skills/mu/recipes/ultrathink.md +0 -86
  117. package/skills/mu/recipes/watcher.md +0 -28
  118. package/skills/mu/recipes/waves.md +0 -21
  119. /package/dist/{chunk-3BARPJLY.js.map → errors-KHOYKD7D.js.map} +0 -0
@@ -1,10 +1,10 @@
1
1
  // extension/mu-pi.ts
2
+ import { randomUUID } from "crypto";
2
3
  import {
3
4
  chmodSync,
4
5
  existsSync as existsSync3,
5
6
  linkSync,
6
7
  mkdirSync as mkdirSync2,
7
- readdirSync,
8
8
  readFileSync as readFileSync3,
9
9
  realpathSync,
10
10
  rmSync,
@@ -12,7 +12,6 @@ import {
12
12
  } from "fs";
13
13
  import { connect, createServer } from "net";
14
14
  import { dirname, join as join4 } from "path";
15
- import { fileURLToPath as fileURLToPath2 } from "url";
16
15
 
17
16
  // src/ctl/protocol.ts
18
17
  var CTL_PROTOCOL_VERSION = 1;
@@ -27,6 +26,10 @@ var CTL_OPS = [
27
26
  "fresh",
28
27
  "command"
29
28
  ];
29
+ var CTL_FEATURES = ["incarnation", "msgId", "fence"];
30
+ var CTL_MSGID_MEMORY = 64;
31
+ var CTL_MSGID_MAX_LENGTH = 64;
32
+ var INCARNATION_MISMATCH = "incarnation mismatch";
30
33
  var UNKNOWN_OP_PREFIX = "unknown op: ";
31
34
  function encode(msg) {
32
35
  return `${JSON.stringify(msg)}
@@ -43,59 +46,72 @@ var LineDecoder = class {
43
46
  };
44
47
 
45
48
  // extension/delegate.ts
46
- import { execFile } from "child_process";
47
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
48
- import { delimiter, join as join2 } from "path";
49
- import { fileURLToPath } from "url";
49
+ import { randomBytes } from "crypto";
50
+ import { statSync } from "fs";
50
51
 
51
- // src/state-dir.ts
52
- import { homedir } from "os";
53
- import { isAbsolute, join } from "path";
54
- function defaultCacheDir() {
55
- const xdg = process.env.XDG_CACHE_HOME;
56
- return join(xdg !== void 0 && isAbsolute(xdg) ? xdg : join(homedir(), ".cache"), "mu");
52
+ // src/workstream-scope.ts
53
+ function workstreamNameProblem(value, flag = "-w/--workstream") {
54
+ if (value.length > 0 && value.trim().length === 0) {
55
+ return `${flag} got a blank (whitespace-only) value ${JSON.stringify(value)}. Pass a workstream name.`;
56
+ }
57
+ if (value.length === 0 || value !== value.trim()) {
58
+ return `${flag} got ${JSON.stringify(value)}: pass a bare workstream name with no surrounding whitespace.`;
59
+ }
60
+ if (/[\s,/\p{Cc}]/u.test(value)) {
61
+ return `${flag} got ${JSON.stringify(value)}: a workstream name has no whitespace, ',' or '/'.`;
62
+ }
63
+ return void 0;
64
+ }
65
+ function qualifiedRefProblem(raw) {
66
+ const slash = raw.indexOf("/");
67
+ if (slash === -1) return void 0;
68
+ const flag = `qualified ref ${JSON.stringify(raw)}:`;
69
+ const prefix = workstreamNameProblem(raw.slice(0, slash), flag);
70
+ if (prefix !== void 0) return prefix;
71
+ const name = raw.slice(slash + 1);
72
+ if (name === "" || name !== name.trim() || /[\s\p{Cc}]/u.test(name)) {
73
+ return `${flag} the name after '/' must be non-empty with no whitespace.`;
74
+ }
75
+ return void 0;
57
76
  }
58
77
 
59
- // extension/delegate.ts
60
- var DELEGATE_TOOL = "mu_delegate";
61
- var DELEGATE_CANCEL_TOOL = "mu_delegate_cancel";
62
- var DELEGATE_MESSAGE_TYPE = "mu-delegate";
63
- var DELEGATE_WORKSTREAM = "scratch";
64
- var DELEGATE_STATUS_KEY = "mu-delegate";
65
- var PENDING_CHANNEL = "murmur:pending";
66
- function delegateStatus(running, starting = 0, queued = 0, failed = 0, pending = 0) {
67
- const parts = [];
68
- if (running > 0) parts.push(`${running} delegate${running === 1 ? "" : "s"} running`);
69
- if (starting > 0) parts.push(`${starting} starting`);
70
- if (queued > 0) parts.push(`${queued} queued`);
71
- if (failed > 0) parts.push(`${failed} failed`);
72
- if (pending > 0) parts.push(`${pending} result${pending === 1 ? "" : "s"} pending`);
73
- return parts.length > 0 ? parts.join(", ") : void 0;
78
+ // extension/delegate-args.ts
79
+ function labelStem(label) {
80
+ if (typeof label !== "string") return void 0;
81
+ const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "");
82
+ return stem || void 0;
74
83
  }
75
- var DELEGATE_TIMEOUT_S = 3600;
76
- var DELEGATE_MAX_DEFAULT = 16;
77
- var DELEGATE_QUEUE_FACTOR = 4;
78
- var PRUNE_FAILED_MS = 3e4;
79
- function delegateMax(env = process.env) {
80
- const n = Number(env.MU_DELEGATE_MAX);
81
- return Number.isInteger(n) && n > 0 ? n : DELEGATE_MAX_DEFAULT;
84
+ var MODEL_BARE_RE = /^[A-Za-z0-9._:/@+=-]+$/;
85
+ function normalizeModel(raw) {
86
+ if (typeof raw !== "string") return void 0;
87
+ let m = raw.trim();
88
+ for (let prev = ""; prev !== m; ) {
89
+ prev = m;
90
+ m = m.replace(/^--model(?:=|\s+)/, "").replace(/^(["'`])(.*)\1$/s, "$2").trim();
91
+ }
92
+ return m && !/\s/.test(m) ? m : void 0;
82
93
  }
83
- var W = DELEGATE_WORKSTREAM;
84
- function delegateEnabled(env = process.env) {
85
- if (env.MU_MANAGED_AGENT) return false;
86
- if (env.MU_DELEGATE === "0") return false;
87
- if (env.MU_MUX || env.HERDR_ENV === "1" || env.TMUX || env.TMUX_PANE) return true;
88
- return onPath("tmux", env) || onPath("herdr", env);
94
+ function shellArg(s) {
95
+ return MODEL_BARE_RE.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
89
96
  }
90
- function isDir(p) {
91
- try {
92
- return statSync(p).isDirectory();
93
- } catch {
94
- return false;
95
- }
97
+ function modelCommand(cli, model, env = process.env) {
98
+ const override = env[`MU_${cli.toUpperCase().replace(/-/g, "_")}_COMMAND`];
99
+ const base = override && override.trim() !== "" ? override.trim() : cli;
100
+ return `${base} --model ${shellArg(model)}`;
96
101
  }
97
- function onPath(bin, env) {
98
- return (env.PATH ?? "").split(delimiter).some((d) => d !== "" && existsSync(join2(d, bin)));
102
+
103
+ // extension/mu-run.ts
104
+ import { execFile } from "child_process";
105
+ import { existsSync } from "fs";
106
+ import { fileURLToPath } from "url";
107
+ function childEnv(over) {
108
+ if (over === void 0) return void 0;
109
+ const env = { ...process.env };
110
+ for (const [k, v] of Object.entries(over)) {
111
+ if (v === void 0) delete env[k];
112
+ else env[k] = v;
113
+ }
114
+ return env;
99
115
  }
100
116
  function defaultRunner() {
101
117
  let cli;
@@ -104,19 +120,180 @@ function defaultRunner() {
104
120
  if (existsSync(p)) cli = p;
105
121
  } catch {
106
122
  }
107
- return (args, signal) => new Promise((resolve) => {
108
- const [cmd, argv] = cli ? [process.execPath, [cli, ...args]] : ["mu", [...args]];
109
- execFile(
123
+ return (args, signal, stdin, opts) => cli ? runChild(process.execPath, [cli, ...args], signal, stdin, opts) : runChild("mu", args, signal, stdin, opts);
124
+ }
125
+ var ABORT_GRACE_MS = 1e4;
126
+ function runChild(cmd, argv, signal, stdin, opts = {}) {
127
+ return new Promise((resolve) => {
128
+ let timer;
129
+ const env = childEnv(opts.env);
130
+ const child = execFile(
110
131
  cmd,
111
- argv,
112
- { maxBuffer: 16 * 1024 * 1024, ...signal ? { signal } : {} },
132
+ [...argv],
133
+ { maxBuffer: 16 * 1024 * 1024, ...env ? { env } : {} },
113
134
  (err, stdout, stderr) => {
114
- const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
135
+ clearTimeout(timer);
136
+ signal?.removeEventListener("abort", onAbort);
137
+ const code = err ? typeof err.code === "number" ? err.code : err.signal ? 143 : 1 : 0;
115
138
  resolve({ code, stdout: String(stdout), stderr: String(stderr || err?.message || "") });
116
139
  }
117
140
  );
141
+ function onAbort() {
142
+ child.kill("SIGTERM");
143
+ timer = setTimeout(() => child.kill("SIGKILL"), opts.drainMs ?? ABORT_GRACE_MS);
144
+ }
145
+ if (signal?.aborted) onAbort();
146
+ else signal?.addEventListener("abort", onAbort, { once: true });
147
+ if (stdin !== void 0) {
148
+ child.stdin?.on("error", () => {
149
+ });
150
+ child.stdin?.end(stdin);
151
+ }
118
152
  });
119
153
  }
154
+ function parseEnvelope(stdout) {
155
+ try {
156
+ const v = JSON.parse(stdout);
157
+ if (typeof v !== "object" || v === null || typeof v.ok !== "boolean") return void 0;
158
+ const data = typeof v.data === "object" && v.data !== null ? v.data : {};
159
+ return v.ok ? data : { ...data, error: v.error, message: v.message };
160
+ } catch {
161
+ return void 0;
162
+ }
163
+ }
164
+ function parseOkEnvelope(stdout) {
165
+ try {
166
+ const v = JSON.parse(stdout);
167
+ if (typeof v !== "object" || v === null || v.ok !== true) return void 0;
168
+ return typeof v.data === "object" && v.data !== null ? v.data : void 0;
169
+ } catch {
170
+ return void 0;
171
+ }
172
+ }
173
+
174
+ // extension/delegate-calls.ts
175
+ var MuCallError = class extends Error {
176
+ constructor(verb, code, reason2) {
177
+ super(`mu delegates ${verb} failed (exit ${code}): ${reason2}`);
178
+ this.verb = verb;
179
+ this.code = code;
180
+ }
181
+ verb;
182
+ code;
183
+ name = "MuCallError";
184
+ };
185
+ var reason = (r) => {
186
+ const m = parseEnvelope(r.stdout)?.message;
187
+ return (typeof m === "string" ? m : r.stderr || r.stdout).trim();
188
+ };
189
+ async function call(run, verb, args, stdin) {
190
+ const r = await run(["delegates", verb, ...args, "--json"], void 0, stdin);
191
+ const out = r.code === 0 ? parseOkEnvelope(r.stdout) : void 0;
192
+ if (out === void 0) throw new MuCallError(verb, r.code, reason(r));
193
+ return out;
194
+ }
195
+ var rowOf = (data) => data.call ?? {};
196
+ function callInfoOf(row) {
197
+ const rec = row.record;
198
+ return {
199
+ callId: String(row.callId),
200
+ caller: String(row.caller),
201
+ workstream: String(row.workstream),
202
+ task: String(row.task),
203
+ ...typeof row.wave === "string" ? { wave: row.wave } : {},
204
+ ...rec?.workstream && rec.task ? { record: { workstream: rec.workstream, task: rec.task } } : {},
205
+ request: row.request ?? { text: "" }
206
+ };
207
+ }
208
+ function callsApi(run) {
209
+ return {
210
+ /** Create the call (task + brief + row, atomically). `created: false`: this id already existed. */
211
+ async register(c, state) {
212
+ const q = c.request;
213
+ const args = [
214
+ c.callId,
215
+ "--caller",
216
+ c.caller,
217
+ "-w",
218
+ c.workstream,
219
+ "--text",
220
+ "-",
221
+ "--state",
222
+ state,
223
+ ...q.label ? ["--label", q.label] : [],
224
+ ...q.cli ? ["--cli", q.cli] : [],
225
+ ...q.model ? ["--model", q.model] : [],
226
+ ...q.cwd ? ["--cwd", q.cwd] : [],
227
+ ...q.workspace === true ? ["--workspace"] : [],
228
+ ...q.timeoutS !== void 0 ? ["--timeout", String(q.timeoutS)] : [],
229
+ ...q.keep === true ? ["--keep"] : [],
230
+ ...c.wave ? ["--wave", c.wave] : [],
231
+ ...c.record ? ["--record", `${c.record.workstream}/${c.record.task}`] : []
232
+ ];
233
+ const data = await call(run, "register", args, q.text);
234
+ const row = rowOf(data);
235
+ return {
236
+ call: callInfoOf(row),
237
+ state: row.state,
238
+ created: data.created === true
239
+ };
240
+ },
241
+ /** One admission step (a compare-and-set on the stored state). */
242
+ async mark(callId, to, extra = {}) {
243
+ await call(run, "mark", [
244
+ callId,
245
+ to,
246
+ ...extra.agent ? ["--agent", extra.agent] : [],
247
+ ...extra.error ? ["--error", extra.error] : []
248
+ ]);
249
+ },
250
+ /** The recorder: verdict note, report and close, in one op group. Idempotent. */
251
+ async finish(callId, o) {
252
+ const data = await call(
253
+ run,
254
+ "finish",
255
+ [
256
+ callId,
257
+ "--outcome",
258
+ o.outcome,
259
+ "--author",
260
+ o.author,
261
+ ...o.elapsedMs !== void 0 ? ["--elapsed-ms", String(Math.round(o.elapsedMs))] : [],
262
+ ...o.record === false ? ["--no-record"] : [],
263
+ ...o.text?.trim() ? ["--text", "-"] : []
264
+ ],
265
+ o.text?.trim() ? o.text : void 0
266
+ );
267
+ return data;
268
+ },
269
+ async show(callId) {
270
+ const row = rowOf(await call(run, "show", [callId]));
271
+ return {
272
+ call: callInfoOf(row),
273
+ state: row.state,
274
+ verdict: typeof row.verdict === "string" ? row.verdict : null
275
+ };
276
+ }
277
+ };
278
+ }
279
+
280
+ // extension/delegate-format.ts
281
+ import { readFileSync } from "fs";
282
+
283
+ // src/delegate-verdict.ts
284
+ var VERDICT_LABELS = ["CONFIRMED", "REFUTED", "UNVERIFIED"];
285
+ function formatElapsed(ms) {
286
+ const t = Math.max(0, Math.round(ms / 1e3));
287
+ if (t < 60) return `${t}s`;
288
+ const m = Math.floor(t / 60);
289
+ if (m < 60) return `${m}m ${String(t % 60).padStart(2, "0")}s`;
290
+ return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
291
+ }
292
+ var LABEL_RE = new RegExp(`\\b(${VERDICT_LABELS.join("|")})\\b`);
293
+
294
+ // extension/delegate-format.ts
295
+ var DELEGATE_WORKSTREAM = "scratch";
296
+ var DELEGATE_TIMEOUT_S = 3600;
120
297
  function lastAssistantTextIn(sessionFile) {
121
298
  let raw;
122
299
  try {
@@ -143,26 +320,17 @@ function lastAssistantTextIn(sessionFile) {
143
320
  return void 0;
144
321
  }
145
322
  function evidenceLines(x) {
146
- return [
147
- x.partial ? `Partial answer so far: ${x.partial}` : void 0,
148
- x.sessionFile ? `Session: ${x.sessionFile}` : void 0
149
- ].filter((v) => v !== void 0);
150
- }
151
- function formatElapsed(ms) {
152
- const t = Math.max(0, Math.round(ms / 1e3));
153
- if (t < 60) return `${t}s`;
154
- const m = Math.floor(t / 60);
155
- if (m < 60) return `${m}m ${String(t % 60).padStart(2, "0")}s`;
156
- return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
323
+ return x.sessionFile ? [`Session: ${x.sessionFile}`] : [];
157
324
  }
158
325
  function delegateMessage(name, agent, pane, x = {}) {
159
326
  const timeoutS = x.timeoutS ?? DELEGATE_TIMEOUT_S;
160
- const who = `Delegate ${W}/${name}${x.queuedAs ? ` (queued as ${x.queuedAs})` : ""}`;
327
+ const W2 = x.workstream ?? DELEGATE_WORKSTREAM;
328
+ const who = `Delegate ${W2}/${name}${x.queuedAs ? ` (queued as ${x.queuedAs})` : ""}`;
161
329
  const took = x.elapsedMs !== void 0 ? ` after ${formatElapsed(x.elapsedMs)}` : "";
162
330
  const fate = pane.closed ? "Pane closed." : `Pane kept (${pane.why}).`;
163
331
  const look = [
164
332
  x.attach ? `Attach: ${x.attach}` : void 0,
165
- `Read: mu agent read ${name} -n 50 -w ${W}`
333
+ `Read: mu agent read ${name} -n 50 -w ${W2}`
166
334
  ].filter((v) => v !== void 0);
167
335
  const tailBlock = x.tail?.trim() ? `
168
336
 
@@ -185,7 +353,7 @@ ${agent.lastText ?? ""}`;
185
353
  "Decide: re-issue the call, or record the check as UNVERIFIED.",
186
354
  ...ev,
187
355
  ...look,
188
- `Close: mu agent close ${name} -w ${W}`
356
+ `Close: mu agent close ${name} -w ${W2}`
189
357
  ].join("\n");
190
358
  case "died":
191
359
  return `${who} died${took} before answering. ${fate}${ws}${evBlock}${tailBlock}`;
@@ -197,8 +365,8 @@ ${agent.lastText ?? ""}`;
197
365
  "Answer the dialog in its pane (attach), then wait again; or close it and record the check as UNVERIFIED.",
198
366
  ...ev,
199
367
  ...look,
200
- `Wait again: mu agent wait ${name} -w ${W} --json${agent.afterRuns !== void 0 ? ` --after-runs ${agent.afterRuns}` : ""}`,
201
- `Close: mu agent close ${name} -w ${W}`
368
+ `Wait again: mu agent wait ${name} -w ${W2} --json${agent.afterRuns !== void 0 ? ` --after-runs ${agent.afterRuns}` : ""}`,
369
+ `Close: mu agent close ${name} -w ${W2}`
202
370
  ];
203
371
  return `${lines.join("\n")}${tailBlock}`;
204
372
  }
@@ -208,86 +376,195 @@ ${agent.lastText ?? ""}`;
208
376
  `${who} is still running after ${timeoutS}s; stopped waiting, so its answer will not arrive here. ${fate}${ws}`,
209
377
  ...ev,
210
378
  ...look,
211
- `Wait again: mu agent wait ${name} -w ${W} --json${agent.afterRuns !== void 0 ? ` --after-runs ${agent.afterRuns}` : ""}`
379
+ `Wait again: mu agent wait ${name} -w ${W2} --json${agent.afterRuns !== void 0 ? ` --after-runs ${agent.afterRuns}` : ""}`
212
380
  ].join("\n");
213
381
  default:
214
382
  return `${who}: mu agent wait gave no result. ${fate}${ws}${evBlock}${tailBlock}`;
215
383
  }
216
384
  }
217
- function droppedAnswersDir() {
218
- return join2(defaultCacheDir(), "delegate-answers");
385
+ function answerLine(a) {
386
+ const how = [a.outcome, a.verdict, a.record ? `recorded on ${a.record}` : void 0].filter((x) => x !== void 0).join(", ");
387
+ const pane = a.kept ? ` Pane kept (${a.kept}): mu agent read ${a.name} -n 50 -w ${a.workstream}.` : "";
388
+ return `- ${a.workstream}/${a.name} (${how}; call ${a.callId}): mu task notes ${a.task} -w ${a.workstream}.${pane}`;
389
+ }
390
+ function wakeText(answers) {
391
+ const n = answers.length;
392
+ return [
393
+ `${n === 1 ? "A delegate answer is" : `${n} delegate answers are`} recorded; the settle shows ${n === 1 ? "it" : "them"} (or read the report):`,
394
+ ...answers.map(answerLine)
395
+ ].join("\n");
219
396
  }
220
397
  function droppedNote(dropped) {
221
398
  const n = dropped.length;
222
- const lines = dropped.map((d) => {
223
- const how = [
224
- d.outcome ?? "no result",
225
- d.elapsedMs !== void 0 ? formatElapsed(d.elapsedMs) : void 0
226
- ].filter((x) => x !== void 0).join(", ");
227
- const where = [
228
- d.copy ? `answer: ${d.copy}` : void 0,
229
- d.sessionFile ? `session: ${d.sessionFile}` : void 0
230
- ].filter((x) => x !== void 0);
231
- if (where.length === 0) where.push(`read: mu agent read ${d.name} -n 50 -w ${W}`);
232
- return `- ${W}/${d.name} (${how}) ${where.join(" \xB7 ")}`;
233
- });
234
399
  return [
235
- `The interrupt dropped ${n} delegate answer${n === 1 ? "" : "s"} before ${n === 1 ? "it" : "they"} arrived:`,
236
- ...lines,
237
- "The answer file holds the message that would have arrived; in a session file the answer is the last assistant message."
400
+ `Interrupted: ${n} delegate answer${n === 1 ? "" : "s"} recorded but not shown; the next settle shows ${n === 1 ? "it" : "them"}:`,
401
+ ...dropped.map(answerLine)
238
402
  ].join("\n");
239
403
  }
240
- var RECORD_NOTE_MAX = 4e3;
241
- var RECORD_FALLBACK_TAIL = 1500;
242
- var VERDICT_LINE = /^[\s>*_`-]*VERDICT:/;
243
- var EVIDENCE_LINE = /^[\s>*_`-]*EVIDENCE:/;
244
- function recordNote(label, name, outcome, lastText, elapsedMs) {
245
- if (outcome !== "done" || !lastText?.trim())
246
- return `REFUTER ${label}: no verdict (${outcome === "done" ? "empty" : outcome ?? "no result"})`;
247
- const lines = lastText.trimEnd().split("\n");
248
- let at = -1;
249
- for (let i = lines.length - 1; i >= 0 && at < 0; i--)
250
- if (VERDICT_LINE.test(lines[i] ?? "")) at = i;
251
- 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)}`;
252
- const note = `REFUTER ${label} (${name}, ${formatElapsed(elapsedMs)}):
253
- ${body}`;
254
- const mark = "\n[truncated]";
255
- return note.length <= RECORD_NOTE_MAX ? note : note.slice(0, RECORD_NOTE_MAX - mark.length) + mark;
404
+
405
+ // extension/delegate-process-format.ts
406
+ function processStarted(call2, cancelTool) {
407
+ const ws = call2.workstream;
408
+ const name = call2.task;
409
+ const text = [
410
+ `Delegated to ${ws}/${name} (call ${call2.callId}) as a child pi process: no multiplexer, so no pane to attach. Its answer arrives with your next settle (and stays on the task: mu task notes ${call2.task} -w ${ws}); carry on, or end your turn if your next step needs it. Do not poll.`,
411
+ call2.request.keep === true ? "keep: true needs a multiplexer; the child stops after its answer (its session file stays)." : void 0,
412
+ `Cancel: ${cancelTool} { name: "${name}" }`
413
+ ].filter((x) => x !== void 0);
414
+ return {
415
+ content: [{ type: "text", text: text.join("\n") }],
416
+ details: {
417
+ name,
418
+ workstream: ws,
419
+ task: `${ws}/${call2.task}`,
420
+ state: "running",
421
+ call_id: call2.callId,
422
+ transport: "process",
423
+ attach: null,
424
+ workspace: null,
425
+ keep: false
426
+ }
427
+ };
256
428
  }
257
- function labelStem(label) {
258
- if (typeof label !== "string") return void 0;
259
- const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "");
260
- return stem || void 0;
429
+ function processFailedText(call2, o, fallback, code, reissueTool) {
430
+ const reason2 = (o.error ?? o.message ?? fallback).replace(/[.\s]+$/, "");
431
+ const next = o.state === "refused" ? `The call is durable and refused: reissue it with ${reissueTool} { call_id: "${call2.callId}" }` : `\`mu delegates run\` exited ${code} without a result; the call is recorded as failed (mu delegates show ${call2.callId})`;
432
+ return `${reason2}. ${next} (task ${call2.workstream}/${call2.task}).`;
261
433
  }
262
- var MODEL_BARE_RE = /^[A-Za-z0-9._:/@+=-]+$/;
263
- function normalizeModel(raw) {
264
- if (typeof raw !== "string") return void 0;
265
- let m = raw.trim();
266
- for (let prev = ""; prev !== m; ) {
267
- prev = m;
268
- m = m.replace(/^--model(?:=|\s+)/, "").replace(/^(["'`])(.*)\1$/s, "$2").trim();
434
+
435
+ // extension/delegate-trace.ts
436
+ import { appendFileSync, mkdirSync } from "fs";
437
+ import { join as join2 } from "path";
438
+
439
+ // src/state-dir.ts
440
+ import { homedir } from "os";
441
+ import { isAbsolute, join } from "path";
442
+ function defaultCacheDir() {
443
+ const xdg = process.env.XDG_CACHE_HOME;
444
+ return join(xdg !== void 0 && isAbsolute(xdg) ? xdg : join(homedir(), ".cache"), "mu");
445
+ }
446
+
447
+ // extension/delegate-trace.ts
448
+ function delegateTraceOn(env = process.env) {
449
+ return !!env.MU_DELEGATE_TRACE && env.MU_DELEGATE_TRACE !== "0";
450
+ }
451
+ function delegateTracePath() {
452
+ return join2(defaultCacheDir(), "delegate-trace.log");
453
+ }
454
+ function trace(event, data = {}) {
455
+ if (!delegateTraceOn()) return;
456
+ try {
457
+ mkdirSync(defaultCacheDir(), { recursive: true });
458
+ const line = { t: (/* @__PURE__ */ new Date()).toISOString(), pid: process.pid, event, ...data };
459
+ appendFileSync(delegateTracePath(), `${JSON.stringify(line)}
460
+ `);
461
+ } catch {
269
462
  }
270
- return m && !/\s/.test(m) ? m : void 0;
271
463
  }
272
- function shellArg(s) {
273
- return MODEL_BARE_RE.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
464
+ var TRACE_EVENTS = [
465
+ "session_start",
466
+ "session_before_switch",
467
+ "session_before_fork",
468
+ "session_before_compact",
469
+ "session_compact",
470
+ "session_compact_failed",
471
+ "session_before_tree",
472
+ "session_tree",
473
+ "agent_start",
474
+ "agent_end",
475
+ "agent_settled",
476
+ "turn_end",
477
+ "message_start",
478
+ "message_end"
479
+ ];
480
+ function traceFields(event, ctx) {
481
+ const e = event ?? {};
482
+ const m = e.message;
483
+ const d = m?.details;
484
+ const out = {};
485
+ if (m?.role) out.role = m.role;
486
+ if (m?.customType) out.customType = m.customType;
487
+ if (d?.delivery !== void 0) out.delivery = d.delivery;
488
+ if (d?.name !== void 0) out.name = d.name;
489
+ for (const k of ["aborted", "reason", "willRetry"]) if (e[k] !== void 0) out[k] = e[k];
490
+ try {
491
+ if (ctx?.isIdle) out.idle = ctx.isIdle();
492
+ if (ctx?.hasPendingMessages) out.pendingMsgs = ctx.hasPendingMessages();
493
+ } catch {
494
+ out.ctx = "stale";
495
+ }
496
+ return out;
274
497
  }
275
- function modelCommand(cli, model, env = process.env) {
276
- const override = env[`MU_${cli.toUpperCase().replace(/-/g, "_")}_COMMAND`];
277
- const base = override && override.trim() !== "" ? override.trim() : cli;
278
- return `${base} --model ${shellArg(model)}`;
498
+
499
+ // extension/delegate.ts
500
+ var DELEGATE_TOOL = "mu_delegate";
501
+ var DELEGATE_CANCEL_TOOL = "mu_delegate_cancel";
502
+ var DELEGATE_REISSUE_TOOL = "mu_delegate_reissue";
503
+ var DELEGATE_MESSAGE_TYPE = "mu-delegate";
504
+ var PENDING_CHANNEL = "murmur:pending";
505
+ var DELEGATE_MAX_DEFAULT = 16;
506
+ var DELEGATE_QUEUE_FACTOR = 4;
507
+ function delegateMax(env = process.env) {
508
+ const n = Number(env.MU_DELEGATE_MAX);
509
+ return Number.isInteger(n) && n > 0 ? n : DELEGATE_MAX_DEFAULT;
510
+ }
511
+ var W = DELEGATE_WORKSTREAM;
512
+ function delegateEnabled(env = process.env) {
513
+ if (env.MU_MANAGED_AGENT) return false;
514
+ return env.MU_DELEGATE !== "0";
515
+ }
516
+ function isDir(p) {
517
+ try {
518
+ return statSync(p).isDirectory();
519
+ } catch {
520
+ return false;
521
+ }
522
+ }
523
+ function piSessionId(ctx) {
524
+ try {
525
+ const sm = ctx?.sessionManager;
526
+ return sm?.getSessionId?.();
527
+ } catch {
528
+ return void 0;
529
+ }
279
530
  }
280
531
  function registerDelegate(pi, run = defaultRunner()) {
281
- if (!delegateEnabled()) return { answerPending: () => false };
532
+ if (!delegateEnabled()) return { answerPending: () => false, delivered: () => {
533
+ } };
534
+ if (delegateTraceOn())
535
+ for (const ev of TRACE_EVENTS)
536
+ pi.on(ev, (e, ctx) => {
537
+ trace(ev, traceFields(e, ctx));
538
+ });
539
+ const send = (message, options, why2) => {
540
+ const d = message.details;
541
+ const a = typeof d?.wake === "number" ? arriving.get(d.wake) : void 0;
542
+ if (a && options) {
543
+ options = { ...options };
544
+ a.message = message;
545
+ a.opts = [...a.opts ?? [], options];
546
+ }
547
+ const at = { why: why2, wake: d?.wake, calls: d?.calls, ...options };
548
+ trace("sendMessage", { ...at, arriving: arriving.size });
549
+ try {
550
+ const r = pi.sendMessage(message, options);
551
+ trace("sendMessage.returned", { ...at, async: r !== void 0 });
552
+ return r;
553
+ } catch (e) {
554
+ trace("sendMessage.threw", { ...at, error: String(e) });
555
+ throw e;
556
+ }
557
+ };
282
558
  const inflight = /* @__PURE__ */ new Map();
283
559
  let starting = 0;
284
560
  const queue = [];
285
561
  let queueSeq = 0;
286
- const failed = /* @__PURE__ */ new Set();
287
- let lastPruneAt = 0;
562
+ const owed = /* @__PURE__ */ new Map();
288
563
  const arriving = /* @__PURE__ */ new Map();
289
- let deliverySeq = 0;
290
- let ui;
564
+ let wakeSeq = 0;
565
+ let busy = false;
566
+ let settleCheck;
567
+ let closed = false;
291
568
  let lastPending = 0;
292
569
  const reportPending = () => {
293
570
  const count = inflight.size + starting + queue.length;
@@ -298,346 +575,447 @@ function registerDelegate(pi, run = defaultRunner()) {
298
575
  } catch {
299
576
  }
300
577
  };
301
- const showStatus = () => {
302
- reportPending();
303
- try {
304
- ui?.setStatus?.(
305
- DELEGATE_STATUS_KEY,
306
- delegateStatus(inflight.size, starting, queue.length, failed.size, arriving.size)
307
- );
308
- } catch {
309
- }
310
- };
311
- const track = (ctx) => {
312
- if (ctx?.hasUI !== false && ctx?.ui) ui = ctx.ui;
313
- };
314
- const markQueued = (delivery) => {
315
- const a = arriving.get(delivery);
316
- if (a) a.queued = true;
317
- };
318
- const saveCopy = (name, content, kind = "") => {
319
- try {
320
- const dir = droppedAnswersDir();
321
- mkdirSync(dir, { recursive: true });
322
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
323
- const path = join2(dir, `${stamp}_${name}${kind}.md`);
324
- writeFileSync(path, `${content}
325
- `);
326
- return path;
327
- } catch {
328
- return void 0;
329
- }
578
+ const wake = (why2) => {
579
+ const covered = new Set([...arriving.values()].flatMap((a) => a.calls));
580
+ const due = [...owed.values()].filter((a) => !covered.has(a.callId));
581
+ if (due.length === 0 || closed) return;
582
+ const id = ++wakeSeq;
583
+ const ids = due.map((a) => a.callId);
584
+ arriving.set(id, { sent: false, calls: ids });
585
+ void Promise.resolve(
586
+ send(
587
+ {
588
+ customType: DELEGATE_MESSAGE_TYPE,
589
+ content: wakeText(due),
590
+ display: true,
591
+ details: { wake: id, calls: ids }
592
+ },
593
+ { deliverAs: "followUp", triggerTurn: true },
594
+ why2
595
+ )
596
+ ).then(
597
+ () => {
598
+ const a = arriving.get(id);
599
+ if (a) a.sent = true;
600
+ },
601
+ () => arriving.delete(id)
602
+ // nothing will arrive; the next settle looks again
603
+ );
330
604
  };
331
- const partialCopy = (name, sessionFile) => {
332
- const text = lastAssistantTextIn(sessionFile);
333
- return text ? saveCopy(name, text, ".partial") : void 0;
605
+ let wakeSoon;
606
+ const wakeIfIdle = () => {
607
+ if (wakeSoon !== void 0) return;
608
+ wakeSoon = setImmediate(() => {
609
+ wakeSoon = void 0;
610
+ if (!busy) wake("wake");
611
+ });
334
612
  };
335
- const forget = (name) => {
336
- inflight.delete(name);
613
+ const calls = callsApi(run);
614
+ const token = randomBytes(3).toString("hex");
615
+ let callSeq = 0;
616
+ const inflightKey = (ws, name) => `${ws}/${name}`;
617
+ const forget = (key) => {
618
+ inflight.delete(key);
337
619
  drain();
338
- showStatus();
620
+ reportPending();
339
621
  };
622
+ const startFailed = (handle, e, what = "Queued delegate") => void Promise.resolve(
623
+ send(
624
+ {
625
+ customType: DELEGATE_MESSAGE_TYPE,
626
+ content: `${what} ${handle} could not start: ${e instanceof Error ? e.message : String(e)}`,
627
+ display: true,
628
+ details: { name: handle, workstream: W, outcome: null, closed: false }
629
+ },
630
+ { deliverAs: "followUp", triggerTurn: true },
631
+ "start-failed"
632
+ )
633
+ ).catch(() => {
634
+ });
340
635
  function drain() {
341
636
  while (queue.length > 0 && inflight.size + starting < delegateMax()) {
342
637
  const q = queue.shift();
343
638
  if (!q) break;
344
639
  starting++;
345
- void start(q.params, void 0, q.ctx, q.handle, q.record).then(
640
+ void q.registered.then(async () => {
641
+ try {
642
+ await calls.mark(q.call.callId, "starting");
643
+ } catch (e) {
644
+ starting--;
645
+ reportPending();
646
+ throw e;
647
+ }
648
+ return start(q.call, void 0, q.ctx, q.handle);
649
+ }).then(
346
650
  () => {
347
651
  },
348
652
  (e) => {
349
- void Promise.resolve(
350
- pi.sendMessage(
351
- {
352
- customType: DELEGATE_MESSAGE_TYPE,
353
- content: `Queued delegate ${q.handle} could not start: ${e instanceof Error ? e.message : String(e)}`,
354
- display: true,
355
- details: { name: q.handle, workstream: W, outcome: null, closed: false }
356
- },
357
- { deliverAs: "followUp", triggerTurn: true }
358
- )
359
- ).catch(() => {
360
- });
653
+ startFailed(q.handle, e);
654
+ drain();
361
655
  }
362
656
  );
363
657
  }
364
658
  }
365
659
  const mu = run;
366
- const json = (r) => {
367
- try {
368
- const v = JSON.parse(r.stdout);
369
- return typeof v === "object" && v !== null ? v : void 0;
370
- } catch {
371
- return void 0;
372
- }
660
+ const json = (r) => parseEnvelope(r.stdout);
661
+ const why = (r) => {
662
+ const m = json(r)?.message;
663
+ return (typeof m === "string" ? m : r.stderr || r.stdout).trim();
373
664
  };
374
- const failure = (what, r) => new Error(`${what} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
375
- function pruneFailed() {
376
- if (failed.size === 0) return;
377
- const now = Date.now();
378
- if (now - lastPruneAt < PRUNE_FAILED_MS) return;
379
- lastPruneAt = now;
380
- void mu(["agent", "list", "-w", W, "--json"]).then((r) => {
381
- const agents = json(r)?.agents;
382
- if (!Array.isArray(agents)) return;
383
- const taken = new Set(agents.map((a) => a.name));
384
- for (const f of [...failed]) if (!taken.has(f)) failed.delete(f);
385
- showStatus();
386
- });
665
+ const failure = (what, r) => new Error(`${what} failed (exit ${r.code}): ${why(r)}`);
666
+ const errText = (e) => e instanceof Error ? e.message : String(e);
667
+ async function closeDelegate(name, id, W2) {
668
+ if (id === void 0) return { closed: false, why: "its spawn did not report an identity" };
669
+ const p = await mu(["agent", "close", name, "-w", W2, "--json"]);
670
+ const d = p.code === 0 ? parseOkEnvelope(p.stdout) : void 0;
671
+ if (d === void 0) return { closed: false, why: `close preview failed: ${why(p)}` };
672
+ if (d.kind === "none") return { closed: false, why: "it is no longer registered" };
673
+ if (d.kind !== "agent" || d.id !== id.incarnation || d.pane_id !== id.paneId)
674
+ return { closed: false, why: "the name now belongs to another agent" };
675
+ if (!Number.isSafeInteger(d.revision))
676
+ return { closed: false, why: "close preview had no revision" };
677
+ const c = await mu(["agent", "close", name, "-w", W2, "--if", String(d.revision), "--json"]);
678
+ return c.code === 0 ? { closed: true } : { closed: false, why: `close refused: ${why(c)}` };
387
679
  }
388
- async function spawn(params, text, cwd, signal) {
389
- signal?.throwIfAborted();
390
- pruneFailed();
391
- const stem = labelStem(params.label);
680
+ class SpawnFail extends Error {
681
+ constructor(message, kind) {
682
+ super(message);
683
+ this.kind = kind;
684
+ }
685
+ kind;
686
+ }
687
+ async function spawn(call2, text, cwd, signal) {
688
+ if (signal?.aborted) throw new SpawnFail("cancelled before it spawned", "aborted");
689
+ const ws = call2.workstream;
690
+ const req = call2.request;
691
+ const stem = labelStem(req.label);
392
692
  const base = stem ? `delegate-${stem}` : "delegate-1";
393
- const args = ["agent", "spawn", base, "-w", W, "--next-free", "--send", text, "--json"];
394
- if (params.workspace === true) args.push("--workspace");
693
+ const args = [
694
+ "agent",
695
+ "spawn",
696
+ base,
697
+ "-w",
698
+ ws,
699
+ "--next-free",
700
+ "--task",
701
+ call2.task,
702
+ "--attempt",
703
+ call2.task,
704
+ "--send",
705
+ text,
706
+ "--json"
707
+ ];
708
+ if (req.workspace === true) args.push("--worktree", ...req.cwd ? ["--root", req.cwd] : []);
395
709
  else if (cwd) args.push("--cwd", cwd);
396
- const cli = typeof params.cli === "string" && params.cli ? params.cli : void 0;
710
+ const cli = req.cli ? req.cli : void 0;
397
711
  if (cli) args.push("--cli", cli);
398
- if (typeof params.model === "string" && params.model)
399
- args.push("--command", modelCommand(cli ?? "pi", params.model));
400
- const r = await mu(args);
712
+ if (req.model) args.push("--command", modelCommand(cli ?? "pi", req.model));
713
+ const r = await mu(args, signal);
401
714
  const out = json(r);
715
+ const stale = out?.task?.stale;
716
+ if (typeof stale === "string")
717
+ throw new SpawnFail(
718
+ `the attempt of ${ws}/${call2.task} is stale: its agent was ${stale}; the replacement was left alone`,
719
+ "unsent"
720
+ );
402
721
  const spawned = out?.agent?.name;
403
- if (typeof spawned !== "string") throw failure("mu agent spawn", r);
722
+ if (typeof spawned !== "string" && out?.error === "NoMultiplexerError")
723
+ throw new SpawnFail("no multiplexer", "nomux");
724
+ if (typeof spawned !== "string" && signal?.aborted)
725
+ throw new SpawnFail("cancelled while it spawned; the spawn rolled back", "aborted");
726
+ if (typeof spawned !== "string") {
727
+ const msg = failure("mu agent spawn", r).message;
728
+ if (out?.error === "WorktreeVcsRequiredError")
729
+ throw new SpawnFail(`${msg}. Pass cwd: <a git repo>`, "refused");
730
+ throw new SpawnFail(msg, out?.error === "UsageError" ? "refused" : "unsent");
731
+ }
404
732
  const name = spawned;
733
+ const row = out?.agent;
734
+ const identity = typeof row?.pane_id === "string" && Number.isSafeInteger(row.id) ? { paneId: row.pane_id, incarnation: row.id } : void 0;
405
735
  if (signal?.aborted) {
406
- await mu(["agent", "close", name, "-w", W]);
407
- signal.throwIfAborted();
736
+ await closeDelegate(name, identity, ws);
737
+ throw new SpawnFail("cancelled while it spawned", "aborted");
408
738
  }
409
- if (out?.ctl !== "ok") {
410
- throw new Error(
411
- `${W}/${name} spawned but its control socket is ${String(out?.ctl)}, so its answer cannot come back. Pane kept; close it with: mu agent close ${name} -w ${W}`
739
+ if (out?.ctl !== void 0 && out.ctl !== "ok") {
740
+ throw new SpawnFail(
741
+ `${ws}/${name} spawned but its control socket is ${String(out.ctl)}, so its answer cannot come back. Pane kept; close it with: mu agent close ${name} -w ${ws}`,
742
+ "kept"
412
743
  );
413
744
  }
414
- const sent = out.send;
745
+ const sent = out?.send;
415
746
  if (sent?.ok !== true && sent?.error === "AgentSendIndeterminateError") {
416
- const why = typeof sent.message === "string" ? sent.message : "no reply";
417
- throw new Error(
418
- `mu agent send to ${W}/${name}: delivery unknown (${why}). Pane kept; do NOT resend blindly. Check with: mu agent read ${name} -w ${W} -n 20 (or attach), then close it with: mu agent close ${name} -w ${W}`
747
+ const reason2 = typeof sent.message === "string" ? sent.message : "no reply";
748
+ throw new SpawnFail(
749
+ `mu agent send to ${ws}/${name}: delivery unknown (${reason2}). Pane kept; do NOT resend blindly. Check with: mu agent read ${name} -w ${ws} -n 20 (or attach), then close it with: mu agent close ${name} -w ${ws}`,
750
+ "unknown"
419
751
  );
420
752
  }
421
753
  if (sent?.ok !== true) {
422
- const c = await mu(["agent", "close", name, "-w", W]);
423
- const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
424
- const why = typeof sent?.message === "string" ? sent.message : (r.stderr || r.stdout).trim();
425
- throw new Error(`mu agent send to ${W}/${name} failed (exit ${r.code}): ${why}. ${pane}`);
754
+ const c = await closeDelegate(name, identity, ws);
755
+ const pane = c.closed ? "Pane closed." : `Pane kept (${c.why}); close it with: mu agent close ${name} -w ${ws}`;
756
+ const reason2 = typeof sent?.message === "string" ? sent.message : why(r);
757
+ throw new SpawnFail(
758
+ `mu agent send to ${ws}/${name} failed (exit ${r.code}): ${reason2}. ${pane}`,
759
+ "unsent"
760
+ );
426
761
  }
427
- const steps = Array.isArray(out.nextSteps) ? out.nextSteps : [];
428
- const attach = steps.map((s) => s).find((s) => s.intent === "Attach the pane")?.command;
429
- const wsPath = out.workspace?.path;
762
+ const attach = out?.attach;
763
+ const wsPath = req.workspace === true ? row?.cwd : void 0;
430
764
  return {
431
765
  name,
432
766
  // A pi just spawned has settled no run, so 0 is exact when an older
433
767
  // extension's reply carries no runs.
434
768
  runs: typeof sent.runs === "number" ? sent.runs : 0,
769
+ ...identity ? { identity } : {},
435
770
  ...typeof attach === "string" ? { attach } : {},
436
771
  ...typeof wsPath === "string" ? { workspace: wsPath } : {},
437
772
  ...typeof sent.sessionFile === "string" ? { sessionFile: sent.sessionFile } : {}
438
773
  };
439
774
  }
440
- const recordLabel = (params, name) => typeof params.label === "string" && params.label.trim() ? params.label.trim() : name;
441
775
  async function resolveRecord(raw) {
442
776
  if (raw === void 0) return void 0;
443
777
  const r = raw;
444
- const task = typeof r?.task === "string" ? r.task.trim() : "";
445
- if (!task) throw new Error("mu_delegate: record.task must name a task");
446
- const ws = typeof r?.workstream === "string" ? r.workstream.trim() : "";
778
+ const task = typeof r?.task === "string" ? r.task : "";
779
+ if (task.trim() === "") throw new Error("mu_delegate: record.task must name a task");
780
+ const scope = (value, what) => {
781
+ const problem = workstreamNameProblem(value, what);
782
+ if (problem !== void 0) throw new Error(`mu_delegate: ${problem} Nothing spawned.`);
783
+ return value;
784
+ };
785
+ const rawWs = r?.workstream;
786
+ const ws = rawWs === void 0 || rawWs === null ? "" : scope(typeof rawWs === "string" ? rawWs : String(rawWs), "record.workstream");
787
+ const slash = task.indexOf("/");
788
+ if (slash >= 0) {
789
+ const refProblem = qualifiedRefProblem(task);
790
+ if (refProblem !== void 0) throw new Error(`mu_delegate: ${refProblem} Nothing spawned.`);
791
+ const prefix = task.slice(0, slash);
792
+ if (ws && ws !== prefix) {
793
+ throw new Error(
794
+ `mu_delegate: record.task ${task} conflicts with record.workstream ${ws}. Nothing spawned.`
795
+ );
796
+ }
797
+ }
798
+ if (task !== task.trim()) {
799
+ throw new Error(
800
+ `mu_delegate: record.task ${JSON.stringify(task)} has surrounding whitespace. Nothing spawned.`
801
+ );
802
+ }
803
+ if (!ws && slash < 0) {
804
+ throw new Error(
805
+ `mu_delegate: record.task ${task} needs a workstream: pass record.task as <workstream>/${task}, or set record.workstream. Nothing spawned.`
806
+ );
807
+ }
447
808
  const shown = await mu(["task", "show", task, ...ws ? ["-w", ws] : [], "--json"]);
448
809
  const out = json(shown);
449
810
  const t = out?.task;
450
- if (shown.code !== 0 || typeof t?.name !== "string" || typeof t.workstreamName !== "string") {
451
- const why = typeof out?.message === "string" ? out.message : (shown.stderr || shown.stdout).trim();
811
+ if (shown.code !== 0 || typeof t?.id !== "string" || typeof t.workstream !== "string") {
452
812
  throw new Error(
453
- `mu_delegate: record task ${ws ? `${ws}/` : ""}${task} not found (exit ${shown.code}): ${why}. Nothing spawned.`
813
+ `mu_delegate: record task ${ws ? `${ws}/` : ""}${task} not found (exit ${shown.code}): ${why(shown)}. Nothing spawned.`
454
814
  );
455
815
  }
456
- return { workstream: t.workstreamName, task: t.name };
816
+ return { workstream: t.workstream, task: t.id };
457
817
  }
458
- async function writeRecord(rec, name, text) {
459
- const where = `${rec.workstream}/${rec.task}`;
818
+ const recordedLine = (call2, err) => `Recording failed: ${err}. The call ${call2.callId} is still open; its answer is in this message. Record it yourself.`;
819
+ async function deliver(call2, name, wait, keep, extras, identity) {
820
+ const ws = call2.workstream;
821
+ const agent = json(wait)?.agents?.[0];
822
+ const outcome = agent?.outcome;
823
+ const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" || outcome === "error" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", ws])).stdout;
824
+ const sessionFile = agent?.sessionFile ?? extras.sessionFile;
825
+ const x = {
826
+ ...extras,
827
+ workstream: ws,
828
+ ...sessionFile ? { sessionFile } : {},
829
+ ...tail ? { tail } : {}
830
+ };
831
+ const partial = outcome !== "done" && outcome !== "blocked" && sessionFile ? lastAssistantTextIn(sessionFile) : void 0;
832
+ const kept = {
833
+ closed: false,
834
+ why: outcome === "blocked" ? "waiting on a dialog" : "kept as evidence"
835
+ };
836
+ const gap = () => [
837
+ delegateMessage(
838
+ name,
839
+ agent,
840
+ outcome === "empty" ? { closed: !keep } : kept,
841
+ x
842
+ ),
843
+ partial ? `Partial answer so far:
844
+ ${partial}` : void 0
845
+ ].filter((v) => v !== void 0).join("\n\n");
846
+ let fin;
847
+ let finErr = "unknown error";
460
848
  try {
461
- const r = await mu(["task", "note", rec.task, "-w", rec.workstream, "--author", name, text]);
462
- if (r.code === 0) return `Recorded on ${where} as a note.`;
463
- return `Recording on ${where} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}. Record it yourself.`;
849
+ fin = await calls.finish(call2.callId, {
850
+ outcome: outcome ?? "no result",
851
+ text: outcome === "done" ? agent?.lastText ?? "" : gap(),
852
+ ...extras.elapsedMs !== void 0 ? { elapsedMs: extras.elapsedMs } : {},
853
+ author: name
854
+ });
464
855
  } catch (e) {
465
- return `Recording on ${where} failed: ${e instanceof Error ? e.message : String(e)}. Record it yourself.`;
856
+ finErr = errText(e);
466
857
  }
467
- }
468
- async function deliver(name, wait, keep, extras, delivery, record) {
469
- const agent = json(wait)?.agents?.[0];
470
- const outcome = agent?.outcome;
471
- const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" || outcome === "error" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
472
858
  let pane = { closed: false, why: "keep: true" };
473
- let closing;
474
- if (outcome === "blocked") pane = { closed: false, why: "waiting on a dialog" };
475
- else if (outcome !== "done" && outcome !== "empty")
476
- pane = { closed: false, why: "kept as evidence" };
477
- else if (!keep) closing = mu(["agent", "close", name, "-w", W]);
478
- const recording = record ? writeRecord(
479
- record.target,
480
- name,
481
- recordNote(record.label, name, outcome, agent?.lastText, extras.elapsedMs ?? 0)
482
- ) : void 0;
483
- const [closed, recorded] = await Promise.all([closing, recording]);
484
- if (closed)
485
- pane = closed.code === 0 ? { closed: true } : { closed: false, why: `close refused: ${(closed.stderr || closed.stdout).trim()}` };
486
- if (outcome === "error" && !pane.closed) {
487
- failed.add(name);
488
- showStatus();
489
- }
490
- if (outcome !== "done") {
491
- const sessionFile = agent?.sessionFile ?? extras.sessionFile;
492
- const partial = sessionFile && outcome !== "blocked" ? partialCopy(name, sessionFile) : void 0;
493
- extras = {
494
- ...extras,
495
- ...sessionFile ? { sessionFile } : {},
496
- ...partial ? { partial } : {}
497
- };
498
- }
499
- const message = delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} });
500
- const content = recorded ? `${message}
859
+ if (outcome === "blocked") pane = kept;
860
+ else if (outcome !== "done" && outcome !== "empty") pane = kept;
861
+ else if (fin?.state !== "done" && outcome === "done")
862
+ pane = { closed: false, why: "its result is not recorded" };
863
+ else if (!keep) pane = await closeDelegate(name, identity, ws);
864
+ if (fin === void 0) {
865
+ await send(
866
+ {
867
+ customType: DELEGATE_MESSAGE_TYPE,
868
+ content: `${delegateMessage(name, agent, pane, x)}
501
869
 
502
- ${recorded}` : message;
503
- const a = arriving.get(delivery);
504
- if (a)
505
- Object.assign(a, {
506
- content,
507
- ...outcome ? { outcome } : {},
508
- ...extras.elapsedMs !== void 0 ? { elapsedMs: extras.elapsedMs } : {},
509
- ...agent?.sessionFile ? { sessionFile: agent.sessionFile } : {}
510
- });
511
- await pi.sendMessage(
512
- {
513
- customType: DELEGATE_MESSAGE_TYPE,
514
- content,
515
- display: true,
516
- details: {
517
- name,
518
- workstream: W,
519
- outcome: outcome ?? null,
520
- closed: pane.closed,
521
- answer: outcome === "done" ? agent?.lastText ?? null : null,
522
- elapsedMs: extras.elapsedMs ?? null,
523
- workspace: extras.workspace ?? null,
524
- sessionFile: agent?.sessionFile ?? extras.sessionFile ?? null,
525
- partial: extras.partial ?? null,
526
- delivery
527
- }
528
- },
529
- { deliverAs: "followUp", triggerTurn: true }
870
+ ${recordedLine(call2, finErr)}`,
871
+ display: true,
872
+ details: { name, workstream: ws, call_id: call2.callId, outcome: outcome ?? null }
873
+ },
874
+ { deliverAs: "followUp", triggerTurn: true },
875
+ "unrecorded"
876
+ );
877
+ return;
878
+ }
879
+ owed.set(call2.callId, {
880
+ callId: call2.callId,
881
+ name,
882
+ workstream: ws,
883
+ task: call2.task,
884
+ // A failed record says so (the answer is kept as the report): record it yourself.
885
+ outcome: fin.state === "failed" && fin.error ? `${outcome ?? "no result"}, NOT recorded: ${fin.error}` : outcome ?? "no result",
886
+ ...fin.verdict ? { verdict: fin.verdict } : {},
887
+ ...call2.record && fin.recorded ? { record: `${call2.record.workstream}/${call2.record.task}` } : {},
888
+ ...pane.closed ? {} : { kept: pane.why },
889
+ at: Date.now()
890
+ });
891
+ wakeIfIdle();
892
+ }
893
+ async function watch(call2, name, runs, o) {
894
+ const ws = call2.workstream;
895
+ const key = inflightKey(ws, name);
896
+ const entry2 = {
897
+ abort: new AbortController(),
898
+ call: call2,
899
+ cancelling: false,
900
+ ...o.sessionFile ? { sessionFile: o.sessionFile } : {},
901
+ ...o.identity ? { identity: o.identity } : {}
902
+ };
903
+ inflight.set(key, entry2);
904
+ if (o.tracked === void 0) reportPending();
905
+ await o.tracked?.();
906
+ const started = Date.now();
907
+ const waiting = mu(
908
+ [
909
+ "agent",
910
+ "wait",
911
+ name,
912
+ "-w",
913
+ ws,
914
+ "--json",
915
+ "--timeout",
916
+ String(o.timeoutS),
917
+ "--after-runs",
918
+ String(runs)
919
+ ],
920
+ entry2.abort.signal
530
921
  );
922
+ const settle = async (r) => {
923
+ trace("settle", { call: call2.callId, name, code: r.code });
924
+ forget(key);
925
+ try {
926
+ await deliver(
927
+ call2,
928
+ name,
929
+ r,
930
+ o.keep,
931
+ {
932
+ timeoutS: o.timeoutS,
933
+ elapsedMs: Date.now() - started,
934
+ ...o.queuedAs ? { queuedAs: o.queuedAs } : {},
935
+ ...o.attach ? { attach: o.attach } : {},
936
+ ...o.workspace ? { workspace: o.workspace } : {},
937
+ ...entry2.sessionFile ? { sessionFile: entry2.sessionFile } : {}
938
+ },
939
+ entry2.identity
940
+ );
941
+ } catch (e) {
942
+ const content = `Delegate ${ws}/${name} settled, but delivering its answer failed: ${e instanceof Error ? e.message : String(e)}. Read it with: mu agent read ${name} -n 50 -w ${ws}`;
943
+ await Promise.resolve(
944
+ send(
945
+ {
946
+ customType: DELEGATE_MESSAGE_TYPE,
947
+ content,
948
+ display: true,
949
+ details: { name, workstream: ws, outcome: null, closed: false }
950
+ },
951
+ { deliverAs: "followUp", triggerTurn: true },
952
+ "deliver-failed"
953
+ )
954
+ ).catch(() => {
955
+ });
956
+ }
957
+ };
958
+ void waiting.then(async (r) => {
959
+ if (inflight.get(key) !== entry2) return;
960
+ if (entry2.cancelling) entry2.parked = () => settle(r);
961
+ else await settle(r);
962
+ });
963
+ return entry2;
531
964
  }
532
- async function start(params, signal, ctx, queuedAs, record) {
965
+ async function start(call2, signal, ctx, queuedAs) {
533
966
  let slotHeld = true;
534
967
  const freeSlot = () => {
535
968
  if (slotHeld) {
536
969
  starting--;
537
- showStatus();
970
+ reportPending();
538
971
  }
539
972
  slotHeld = false;
540
973
  };
541
- showStatus();
974
+ reportPending();
975
+ const ws = call2.workstream;
976
+ const req = call2.request;
542
977
  try {
543
- const task = typeof params.task === "string" ? params.task.trim() : "";
544
- const brief = typeof params.brief === "string" ? params.brief.trim() : "";
545
- const keep = params.keep === true;
546
- const t = params.timeout;
547
- if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
978
+ const keep = req.keep === true;
979
+ const timeoutS = req.timeoutS ?? DELEGATE_TIMEOUT_S;
980
+ let cwd;
981
+ if (req.workspace !== true) {
982
+ cwd = req.cwd ? req.cwd : ctx?.cwd ?? process.cwd();
983
+ if (!isDir(cwd))
984
+ throw new SpawnFail(`mu_delegate: cwd ${cwd} is not a directory`, "unsent");
985
+ }
986
+ let spawned;
987
+ try {
988
+ spawned = await spawn(call2, req.text, cwd, signal);
989
+ } catch (e) {
990
+ if (e instanceof SpawnFail && e.kind === "nomux") {
991
+ freeSlot();
992
+ return startProcess(call2);
993
+ }
994
+ const fail2 = e instanceof SpawnFail ? e : new SpawnFail(errText(e), "unsent");
995
+ await settleFailedStart(call2, fail2);
548
996
  throw new Error(
549
- `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
997
+ fail2.kind === "unsent" ? `${fail2.message}. The call is durable and refused: reissue it with ${DELEGATE_REISSUE_TOOL} { call_id: "${call2.callId}" } (task ${ws}/${call2.task}).` : `${fail2.message}. Recorded on ${ws}/${call2.task} as failed.`
550
998
  );
551
- const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
552
- let cwd;
553
- if (params.workspace !== true) {
554
- cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
555
- if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
556
999
  }
557
- const { name, runs, attach, workspace, sessionFile } = await spawn(
558
- params,
559
- brief ? `${brief}
560
-
561
- ${task}` : task,
562
- cwd,
563
- signal
564
- );
565
- const entry = {
566
- abort: new AbortController(),
567
- cancelling: false,
1000
+ const { name, runs, attach, workspace, sessionFile, identity } = spawned;
1001
+ await watch(call2, name, runs, {
1002
+ keep,
1003
+ timeoutS,
1004
+ ...queuedAs ? { queuedAs } : {},
1005
+ ...attach ? { attach } : {},
1006
+ ...workspace ? { workspace } : {},
568
1007
  ...sessionFile ? { sessionFile } : {},
569
- ...record ? { record: { target: record, label: recordLabel(params, name) } } : {}
570
- };
571
- inflight.set(name, entry);
572
- freeSlot();
573
- showStatus();
574
- const started = Date.now();
575
- const waiting = mu(
576
- [
577
- "agent",
578
- "wait",
579
- name,
580
- "-w",
581
- W,
582
- "--json",
583
- "--timeout",
584
- String(timeoutS),
585
- "--after-runs",
586
- String(runs)
587
- ],
588
- entry.abort.signal
589
- );
590
- const settle = async (r) => {
591
- const delivery = ++deliverySeq;
592
- arriving.set(delivery, { queued: false, name });
593
- forget(name);
594
- try {
595
- await deliver(
596
- name,
597
- r,
598
- keep,
599
- {
600
- timeoutS,
601
- elapsedMs: Date.now() - started,
602
- ...queuedAs ? { queuedAs } : {},
603
- ...attach ? { attach } : {},
604
- ...workspace ? { workspace } : {},
605
- ...entry.sessionFile ? { sessionFile: entry.sessionFile } : {}
606
- },
607
- delivery,
608
- entry.record
609
- );
610
- markQueued(delivery);
611
- } catch (e) {
612
- const 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}`;
613
- const a = arriving.get(delivery);
614
- if (a) a.content = content;
615
- await Promise.resolve(
616
- pi.sendMessage(
617
- {
618
- customType: DELEGATE_MESSAGE_TYPE,
619
- content,
620
- display: true,
621
- details: { name, workstream: W, outcome: null, closed: false, delivery }
622
- },
623
- { deliverAs: "followUp", triggerTurn: true }
624
- )
625
- ).then(
626
- () => markQueued(delivery),
627
- () => {
628
- arriving.delete(delivery);
629
- showStatus();
630
- }
631
- );
1008
+ ...identity ? { identity } : {},
1009
+ tracked: async () => {
1010
+ freeSlot();
1011
+ reportPending();
1012
+ await calls.mark(call2.callId, "running", { agent: name }).catch((e) => {
1013
+ trace("mark.running.failed", { call: call2.callId, error: errText(e) });
1014
+ });
632
1015
  }
633
- };
634
- void waiting.then(async (r) => {
635
- if (inflight.get(name) !== entry) return;
636
- if (entry.cancelling) entry.parked = () => settle(r);
637
- else await settle(r);
638
1016
  });
639
1017
  const lines = [
640
- `Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
1018
+ `Delegated to ${ws}/${name} (task ${ws}/${call2.task}, call ${call2.callId}). Its answer arrives with your next settle (and stays on the task: mu task notes ${call2.task} -w ${ws}); carry on, or end your turn if your next step needs it. Do not poll.`,
641
1019
  workspace ? `Workspace: ${workspace}` : void 0,
642
1020
  attach ? `Attach: ${attach}` : void 0,
643
1021
  `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
@@ -646,7 +1024,10 @@ ${task}` : task,
646
1024
  content: [{ type: "text", text: lines.join("\n") }],
647
1025
  details: {
648
1026
  name,
649
- workstream: W,
1027
+ workstream: ws,
1028
+ task: `${ws}/${call2.task}`,
1029
+ state: "running",
1030
+ call_id: call2.callId,
650
1031
  attach: attach ?? null,
651
1032
  workspace: workspace ?? null,
652
1033
  keep
@@ -657,11 +1038,92 @@ ${task}` : task,
657
1038
  drain();
658
1039
  }
659
1040
  }
1041
+ const finishOrphan = (call2, outcome, text) => calls.finish(call2.callId, { outcome, text, author: call2.caller }).catch((e) => {
1042
+ trace("finishOrphan.failed", { call: call2.callId, error: errText(e) });
1043
+ return void 0;
1044
+ });
1045
+ function startProcess(call2) {
1046
+ const ws = call2.workstream;
1047
+ const name = call2.task;
1048
+ const key = inflightKey(ws, name);
1049
+ const entry2 = { abort: new AbortController(), call: call2, cancelling: false };
1050
+ inflight.set(key, entry2);
1051
+ reportPending();
1052
+ const settle = async (r) => {
1053
+ const o = json(r) ?? {};
1054
+ forget(key);
1055
+ if (o.outcome === void 0 || o.state === "refused") {
1056
+ const text = processFailedText(call2, o, why(r), r.code, DELEGATE_REISSUE_TOOL);
1057
+ if (o.state !== "refused") await finishOrphan(call2, "error", text);
1058
+ startFailed(name, new Error(text), "Delegate");
1059
+ return;
1060
+ }
1061
+ const verdict = await calls.show(call2.callId).then((x) => x.verdict ?? void 0).catch(() => void 0);
1062
+ owed.set(call2.callId, {
1063
+ callId: call2.callId,
1064
+ name,
1065
+ workstream: ws,
1066
+ task: call2.task,
1067
+ outcome: o.outcome,
1068
+ ...verdict ? { verdict } : {},
1069
+ ...call2.record && o.state === "done" ? { record: `${call2.record.workstream}/${call2.record.task}` } : {},
1070
+ at: Date.now()
1071
+ });
1072
+ wakeIfIdle();
1073
+ };
1074
+ const running = mu(
1075
+ ["delegates", "run", call2.callId, "--no-keep", "--json"],
1076
+ entry2.abort.signal,
1077
+ void 0,
1078
+ { drainMs: 2e4 }
1079
+ );
1080
+ entry2.process = running;
1081
+ void running.then(async (r) => {
1082
+ if (inflight.get(key) !== entry2) return;
1083
+ await settle(r);
1084
+ });
1085
+ return processStarted(call2, DELEGATE_CANCEL_TOOL);
1086
+ }
1087
+ async function settleFailedStart(call2, fail2) {
1088
+ try {
1089
+ if (fail2.kind === "unsent") await calls.mark(call2.callId, "refused", { error: fail2.message });
1090
+ else
1091
+ await calls.finish(call2.callId, {
1092
+ outcome: fail2.kind === "aborted" ? "cancelled" : fail2.kind === "unknown" ? "unknown" : "error",
1093
+ text: fail2.message,
1094
+ author: call2.caller
1095
+ });
1096
+ } catch (e) {
1097
+ trace("settleFailedStart.failed", { call: call2.callId, error: errText(e) });
1098
+ }
1099
+ }
1100
+ const callIdIn = (name) => name.startsWith("pi:");
1101
+ const admission = () => {
1102
+ const max = delegateMax();
1103
+ if (inflight.size + starting < max) return "starting";
1104
+ return queue.length < DELEGATE_QUEUE_FACTOR * max ? "queued" : "refused";
1105
+ };
1106
+ const queuedResult = (call2, state, handle, position) => {
1107
+ const max = delegateMax();
1108
+ const taskRef = `${call2.workstream}/${call2.task}`;
1109
+ const text = state === "refused" ? `REFUSED, not started: ${inflight.size + starting} running and ${queue.length} queued (MU_DELEGATE_MAX=${max}). The call is durable (task ${taskRef}, call ${call2.callId}) and will NOT start on its own. Reissue it once slots free, as answers arrive: ${DELEGATE_REISSUE_TOOL} { call_id: "${call2.callId}" }. List: mu delegates.` : `Queued as ${handle} (#${position}; ${inflight.size + starting} running, MU_DELEGATE_MAX=${max}; task ${taskRef}, call ${call2.callId}). It starts when a slot frees, and its answer arrives with a settle like any delegate's; do not poll. Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${handle}" }`;
1110
+ return {
1111
+ content: [{ type: "text", text }],
1112
+ details: {
1113
+ name: handle,
1114
+ workstream: call2.workstream,
1115
+ task: taskRef,
1116
+ state,
1117
+ call_id: call2.callId,
1118
+ ...state === "queued" ? { queued: true, position } : { refused: true }
1119
+ }
1120
+ };
1121
+ };
660
1122
  pi.registerTool({
661
1123
  name: DELEGATE_TOOL,
662
1124
  label: "mu delegate",
663
- 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.`,
664
- promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
1125
+ description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream; with no tmux or herdr, a child pi process with no pane) 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; past that a call is refused: durable, reissue it with ${DELEGATE_REISSUE_TOOL}). Every call is a mu task (kind delegate): the result names it, and the full answer stays on it. 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, with a settle (a wave's as ONE summary): carry on with other work, or end your turn if your next step needs the answer (it resumes you). The pane closes after a clean finish; keep: true leaves it open (needs a mux). When the check judges a mu task, set record so its verdict lands on that task; leave it out when there is no task. Set wave to group a fan-out (mu delegates tally).`,
1126
+ promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later, with a settle",
665
1127
  parameters: {
666
1128
  type: "object",
667
1129
  properties: {
@@ -676,16 +1138,16 @@ ${task}` : task,
676
1138
  },
677
1139
  cwd: {
678
1140
  type: "string",
679
- description: "Working directory for the delegate (default: yours; ignored with workspace)"
1141
+ description: "Working directory for the delegate (default: yours); with workspace, the git repo the worktree is made from"
680
1142
  },
681
1143
  timeout: {
682
1144
  type: "number",
683
1145
  exclusiveMinimum: 0,
684
- 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.`
1146
+ description: `Seconds to wait for the answer (default ${DELEGATE_TIMEOUT_S}). On timeout you get a "still running" answer instead, and the real answer will not arrive later; the subagent keeps running in its pane.`
685
1147
  },
686
1148
  workspace: {
687
1149
  type: "boolean",
688
- 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."
1150
+ description: "For tasks that edit files: its own git worktree branched from the current commit of the repo at cwd (default: yours). The result names 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."
689
1151
  },
690
1152
  cli: {
691
1153
  type: "string",
@@ -697,24 +1159,31 @@ ${task}` : task,
697
1159
  },
698
1160
  keep: {
699
1161
  type: "boolean",
700
- description: "Keep the pane after it finishes, to talk to it again"
1162
+ description: "Keep the pane after it finishes, to talk to it again (needs a mux; ignored without one)"
701
1163
  },
702
1164
  record: {
703
1165
  type: "object",
704
- 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>.",
1166
+ 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); name the workstream: task as <ws>/<task>, or set workstream.",
705
1167
  properties: {
706
1168
  task: { type: "string" },
707
1169
  workstream: { type: "string" }
708
1170
  },
709
1171
  required: ["task"],
710
1172
  additionalProperties: false
1173
+ },
1174
+ wave: {
1175
+ type: "string",
1176
+ description: "Label grouping the calls of one fan-out; `mu delegates tally -w <workstream> --wave <label>` counts their verdicts per judged task."
1177
+ },
1178
+ workstream: {
1179
+ type: "string",
1180
+ description: `Workstream that holds the delegate task and runs the delegate (default: ${W}).`
711
1181
  }
712
1182
  },
713
1183
  required: ["task"],
714
1184
  additionalProperties: false
715
1185
  },
716
1186
  async execute(_id, params, signal, _onUpdate, ctx) {
717
- track(ctx);
718
1187
  const task = typeof params.task === "string" ? params.task.trim() : "";
719
1188
  if (!task) throw new Error("mu_delegate needs a non-empty task");
720
1189
  const t = params.timeout;
@@ -722,179 +1191,435 @@ ${task}` : task,
722
1191
  throw new Error(
723
1192
  `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
724
1193
  );
725
- if (params.workspace !== true) {
726
- const cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
727
- if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
1194
+ const callCwd = typeof params.cwd === "string" && params.cwd ? params.cwd : params.workspace !== true ? ctx?.cwd ?? process.cwd() : void 0;
1195
+ if (callCwd !== void 0 && !isDir(callCwd))
1196
+ throw new Error(`mu_delegate: cwd ${callCwd} is not a directory`);
1197
+ let model;
1198
+ if (params.model !== void 0 && params.model !== "") {
1199
+ model = normalizeModel(params.model);
1200
+ if (!model)
1201
+ throw new Error(
1202
+ `mu_delegate: model must be one pi model id like 'provider/id:thinking', as listed by \`pi --list-models\` (got ${JSON.stringify(params.model)}); omit it for the cli's default`
1203
+ );
1204
+ }
1205
+ let workstream = W;
1206
+ if (params.workstream !== void 0) {
1207
+ const problem = workstreamNameProblem(
1208
+ typeof params.workstream === "string" ? params.workstream : String(params.workstream),
1209
+ "workstream"
1210
+ );
1211
+ if (problem !== void 0) throw new Error(`mu_delegate: ${problem} Nothing spawned.`);
1212
+ workstream = params.workstream;
1213
+ }
1214
+ if (params.wave !== void 0 && (typeof params.wave !== "string" || params.wave.trim() === ""))
1215
+ throw new Error(
1216
+ `mu_delegate: wave must be a non-empty label (got ${JSON.stringify(params.wave)})`
1217
+ );
1218
+ const record = await resolveRecord(params.record);
1219
+ const brief = typeof params.brief === "string" ? params.brief.trim() : "";
1220
+ const label = typeof params.label === "string" && params.label.trim() ? params.label.trim() : void 0;
1221
+ const decision = admission();
1222
+ const handle = decision === "starting" ? "" : `queued-${++queueSeq}`;
1223
+ if (decision === "starting") starting++;
1224
+ const session = piSessionId(ctx) || process.env.PI_SESSION_ID || `local-${token}`;
1225
+ const caller = `pi:${session}`;
1226
+ const info = {
1227
+ callId: `${caller}:${token}-${++callSeq}`,
1228
+ caller,
1229
+ workstream,
1230
+ ...typeof params.wave === "string" ? { wave: params.wave.trim() } : {},
1231
+ ...record ? { record } : {},
1232
+ request: {
1233
+ text: brief ? `${brief}
1234
+
1235
+ ${task}` : task,
1236
+ ...label ? { label } : {},
1237
+ ...typeof params.cli === "string" && params.cli ? { cli: params.cli } : {},
1238
+ ...model ? { model } : {},
1239
+ ...callCwd !== void 0 ? { cwd: callCwd } : {},
1240
+ ...params.workspace === true ? { workspace: true } : {},
1241
+ ...typeof t === "number" ? { timeoutS: Math.ceil(t) } : {},
1242
+ ...params.keep === true ? { keep: true } : {}
1243
+ }
1244
+ };
1245
+ if (decision === "starting") {
1246
+ let reg;
1247
+ try {
1248
+ reg = await calls.register(info, "starting");
1249
+ } catch (e) {
1250
+ starting--;
1251
+ reportPending();
1252
+ drain();
1253
+ throw new Error(
1254
+ `mu_delegate: could not register the call: ${errText(e)}. Nothing spawned.`
1255
+ );
1256
+ }
1257
+ return start(reg.call, signal, ctx);
728
1258
  }
729
- if (params.model !== void 0 && params.model !== "") {
730
- const m = normalizeModel(params.model);
731
- if (!m)
1259
+ if (decision === "refused") {
1260
+ let reg;
1261
+ try {
1262
+ reg = await calls.register(info, "refused");
1263
+ } catch (e) {
732
1264
  throw new Error(
733
- `mu_delegate: model must be one pi model id like 'provider/id:thinking', as listed by \`pi --list-models\` (got ${JSON.stringify(params.model)}); omit it for the cli's default`
1265
+ `mu_delegate: could not register the call: ${errText(e)}. Nothing spawned.`
734
1266
  );
735
- params = { ...params, model: m };
1267
+ }
1268
+ return queuedResult(reg.call, "refused", handle, 0);
736
1269
  }
737
- const record = await resolveRecord(params.record);
738
- const max = delegateMax();
739
- if (inflight.size + starting < max) {
740
- starting++;
741
- return start(params, signal, ctx, void 0, record);
1270
+ const entry2 = {
1271
+ handle,
1272
+ call: { ...info, task: "" },
1273
+ ctx,
1274
+ registered: Promise.resolve()
1275
+ };
1276
+ entry2.registered = calls.register(info, "queued").then((reg) => {
1277
+ entry2.call = reg.call;
1278
+ });
1279
+ queue.push(entry2);
1280
+ reportPending();
1281
+ try {
1282
+ await entry2.registered;
1283
+ } catch (e) {
1284
+ const at = queue.indexOf(entry2);
1285
+ if (at >= 0) queue.splice(at, 1);
1286
+ reportPending();
1287
+ throw new Error(`mu_delegate: could not register the call: ${errText(e)}. Nothing queued.`);
742
1288
  }
743
- if (queue.length >= DELEGATE_QUEUE_FACTOR * max)
1289
+ return queuedResult(entry2.call, "queued", handle, queue.indexOf(entry2) + 1 || queue.length);
1290
+ }
1291
+ });
1292
+ pi.registerTool({
1293
+ name: DELEGATE_REISSUE_TOOL,
1294
+ label: "mu delegate reissue",
1295
+ description: `Run a call ${DELEGATE_TOOL} refused (the queue was full): same call, same task, never a second verdict. Takes the call_id from the refusal; starts now if a slot is free, else queues.`,
1296
+ parameters: {
1297
+ type: "object",
1298
+ properties: { call_id: { type: "string", description: "The refused call's id" } },
1299
+ required: ["call_id"],
1300
+ additionalProperties: false
1301
+ },
1302
+ async execute(_id, params, signal, _onUpdate, ctx) {
1303
+ const callId = typeof params.call_id === "string" ? params.call_id : "";
1304
+ if (!callId) throw new Error(`${DELEGATE_REISSUE_TOOL} needs a call_id`);
1305
+ const shown = await calls.show(callId).catch((e) => {
1306
+ throw new Error(`${DELEGATE_REISSUE_TOOL}: ${errText(e)}`);
1307
+ });
1308
+ if (shown.state !== "refused")
1309
+ throw new Error(`${DELEGATE_REISSUE_TOOL}: call ${callId} is ${shown.state}, not refused.`);
1310
+ const decision = admission();
1311
+ if (decision === "refused")
744
1312
  throw new Error(
745
- `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).`
1313
+ `${DELEGATE_REISSUE_TOOL}: still full (${inflight.size + starting} running, ${queue.length} queued). Nothing changed; reissue ${callId} when an answer arrives.`
746
1314
  );
1315
+ if (decision === "starting") starting++;
1316
+ try {
1317
+ await calls.mark(callId, decision === "starting" ? "starting" : "queued");
1318
+ } catch (e) {
1319
+ if (decision === "starting") {
1320
+ starting--;
1321
+ reportPending();
1322
+ drain();
1323
+ }
1324
+ throw new Error(`${DELEGATE_REISSUE_TOOL}: ${errText(e)}`);
1325
+ }
1326
+ if (decision === "starting") return start(shown.call, signal, ctx);
747
1327
  const handle = `queued-${++queueSeq}`;
748
- queue.push({
749
- handle,
750
- params: {
751
- ...params,
752
- cwd: typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd()
753
- },
754
- ctx,
755
- ...record ? { record } : {}
756
- });
757
- showStatus();
758
- return {
759
- content: [
760
- {
761
- type: "text",
762
- 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}" }`
763
- }
764
- ],
765
- details: { name: handle, workstream: W, queued: true, position: queue.length }
766
- };
1328
+ queue.push({ handle, call: shown.call, ctx, registered: Promise.resolve() });
1329
+ reportPending();
1330
+ return queuedResult(shown.call, "queued", handle, queue.length);
767
1331
  }
768
1332
  });
769
1333
  pi.registerTool({
770
1334
  name: DELEGATE_CANCEL_TOOL,
771
1335
  label: "mu delegate cancel",
772
- description: `Stop a delegate started by ${DELEGATE_TOOL}: abort its turn and close its pane (keep: true leaves the pane open). No follow-up message is sent for it afterwards.`,
1336
+ description: `Stop a delegate started by ${DELEGATE_TOOL}: abort its turn and close its pane (keep: true leaves the pane open; a mux-free one's child pi is stopped). A queued or refused call is cancelled too (name it by its handle or call_id). The verdict target, if any, gets "no verdict (cancelled)". Its answer is not shown afterwards.`,
773
1337
  parameters: {
774
1338
  type: "object",
775
1339
  properties: {
776
- name: { type: "string", description: "Delegate name, e.g. delegate-1" },
1340
+ name: {
1341
+ type: "string",
1342
+ description: "Delegate name (delegate-1), queue handle (queued-2) or call_id"
1343
+ },
777
1344
  keep: { type: "boolean", description: "Leave the pane open after aborting" }
778
1345
  },
779
1346
  required: ["name"],
780
1347
  additionalProperties: false
781
1348
  },
782
- async execute(_id, params, _signal, _onUpdate, ctx) {
783
- track(ctx);
1349
+ async execute(_id, params) {
784
1350
  const name = typeof params.name === "string" ? params.name : "";
785
1351
  if (!name) throw new Error("mu_delegate_cancel needs a name");
786
- const qi = queue.findIndex((q) => q.handle === name);
1352
+ const cancelCall = async (call2, author, text2) => {
1353
+ try {
1354
+ const f = await calls.finish(call2.callId, {
1355
+ outcome: "cancelled",
1356
+ ...text2 ? { text: text2 } : {},
1357
+ author
1358
+ });
1359
+ if (f.recorded && call2.record)
1360
+ return ` Recorded on ${call2.record.workstream}/${call2.record.task} as a note (no verdict: cancelled).`;
1361
+ return f.changed ? ` Task ${call2.workstream}/${call2.task} closed (cancelled).` : "";
1362
+ } catch (e) {
1363
+ return ` Recording the cancel failed: ${errText(e)}.`;
1364
+ }
1365
+ };
1366
+ const qi = queue.findIndex((q) => q.handle === name || q.call.callId === name);
787
1367
  if (qi >= 0) {
788
1368
  const [q] = queue.splice(qi, 1);
789
- showStatus();
790
- const recorded = q?.record ? ` ${await writeRecord(q.record, name, recordNote(recordLabel(q.params, name), name, "cancelled", void 0, 0))}` : "";
1369
+ reportPending();
1370
+ let recorded = "";
1371
+ if (q) {
1372
+ await q.registered.catch(() => {
1373
+ });
1374
+ recorded = await cancelCall(q.call, q.handle);
1375
+ }
791
1376
  return {
792
1377
  content: [
793
1378
  { type: "text", text: `Dropped ${name} from the queue; it never started.${recorded}` }
794
1379
  ],
795
- details: { name, workstream: W }
1380
+ details: { name, workstream: q?.call.workstream ?? W }
1381
+ };
1382
+ }
1383
+ const found = [...inflight].find(
1384
+ ([key2, e]) => key2 === name || key2 === inflightKey(e.call.workstream, name) || e.call.callId === name
1385
+ );
1386
+ if (!found && callIdIn(name)) {
1387
+ const shown = await calls.show(name).catch((e) => {
1388
+ throw new Error(`mu_delegate_cancel: ${errText(e)}`);
1389
+ });
1390
+ const recorded = await cancelCall(shown.call, name);
1391
+ return {
1392
+ content: [{ type: "text", text: `Cancelled ${name} (${shown.state}).${recorded}` }],
1393
+ details: { name, workstream: shown.call.workstream }
1394
+ };
1395
+ }
1396
+ const key = found?.[0];
1397
+ const entry2 = found?.[1];
1398
+ const ws = entry2?.call.workstream ?? W;
1399
+ const agentName = key ? key.slice(key.indexOf("/") + 1) : name;
1400
+ if (entry2?.process && key) {
1401
+ entry2.abort.abort();
1402
+ forget(key);
1403
+ await entry2.process;
1404
+ await finishOrphan(entry2.call, "cancelled", "cancelled");
1405
+ const shown = await calls.show(entry2.call.callId).catch(() => void 0);
1406
+ const where = entry2.call.record ? ` Recorded on ${entry2.call.record.workstream}/${entry2.call.record.task} as a note (no verdict: cancelled).` : "";
1407
+ return {
1408
+ content: [
1409
+ {
1410
+ type: "text",
1411
+ text: `Cancelled ${ws}/${agentName} (child pi stopped; call ${shown?.state ?? "cancelling"}).${where}`
1412
+ }
1413
+ ],
1414
+ details: { name: agentName, workstream: ws }
796
1415
  };
797
1416
  }
798
- const entry = inflight.get(name);
799
- if (entry) entry.cancelling = true;
800
- const ab = await mu(["agent", "abort", name, "-w", W]);
1417
+ if (entry2) entry2.cancelling = true;
1418
+ const ab = await mu(["agent", "abort", agentName, "-w", ws]);
801
1419
  if (ab.code !== 0) {
802
- if (entry) {
803
- entry.cancelling = false;
804
- const parked = entry.parked;
805
- entry.parked = void 0;
1420
+ if (entry2) {
1421
+ entry2.cancelling = false;
1422
+ const parked = entry2.parked;
1423
+ entry2.parked = void 0;
806
1424
  if (parked) void parked();
807
1425
  }
808
- throw failure(`mu agent abort ${W}/${name}`, ab);
1426
+ throw failure(`mu agent abort ${ws}/${agentName}`, ab);
809
1427
  }
810
- if (entry) {
811
- entry.abort.abort();
812
- forget(name);
1428
+ if (entry2 && key) {
1429
+ entry2.abort.abort();
1430
+ forget(key);
813
1431
  }
814
- failed.delete(name);
815
- let text = `Aborted ${W}/${name}.`;
816
- const sessionFile = entry?.sessionFile;
817
- const partial = sessionFile ? partialCopy(name, sessionFile) : void 0;
818
- const evidence = evidenceLines({
819
- ...sessionFile ? { sessionFile } : {},
820
- ...partial ? { partial } : {}
821
- });
822
- if (entry?.record)
823
- text += ` ${await writeRecord(entry.record.target, name, recordNote(entry.record.label, name, "cancelled", void 0, 0))}`;
1432
+ let text = `Aborted ${ws}/${agentName}.`;
1433
+ const sessionFile = entry2?.sessionFile;
1434
+ const evidence = evidenceLines({ ...sessionFile ? { sessionFile } : {} });
1435
+ if (entry2)
1436
+ text += await cancelCall(
1437
+ entry2.call,
1438
+ agentName,
1439
+ sessionFile ? lastAssistantTextIn(sessionFile) : void 0
1440
+ );
824
1441
  if (params.keep === true) text += " Pane kept.";
825
1442
  else {
826
- const c = await mu(["agent", "close", name, "-w", W]);
827
- text += c.code === 0 ? " Pane closed." : ` Pane kept: close refused: ${(c.stderr || c.stdout).trim()}`;
1443
+ const c = await closeDelegate(agentName, entry2?.identity, ws);
1444
+ text += c.closed ? " Pane closed." : ` Pane kept: ${c.why}`;
828
1445
  }
829
1446
  if (evidence.length > 0) text += `
830
1447
  ${evidence.join("\n")}`;
831
- return { content: [{ type: "text", text }], details: { name, workstream: W } };
1448
+ return { content: [{ type: "text", text }], details: { name: agentName, workstream: ws } };
832
1449
  }
833
1450
  });
834
1451
  pi.on("message_start", (e) => {
835
1452
  const m = e.message;
836
1453
  if (m?.role !== "custom" || m.customType !== DELEGATE_MESSAGE_TYPE) return;
837
- const id = m.details?.delivery;
838
- if (typeof id === "number" && arriving.delete(id)) showStatus();
1454
+ const id = m.details?.wake;
1455
+ if (typeof id !== "number") return;
1456
+ for (const c of arriving.get(id)?.calls ?? []) owed.delete(c);
1457
+ arriving.delete(id);
839
1458
  });
840
- pi.on("agent_settled", (e) => {
841
- if (e.aborted !== true) return;
842
- const dropped = [];
843
- for (const [id, a] of arriving) {
844
- if (!a.queued) continue;
1459
+ const dropWakes = (ids, also = []) => {
1460
+ const names = new Set(also);
1461
+ for (const id of ids) {
1462
+ const a = arriving.get(id);
1463
+ if (!a) continue;
845
1464
  arriving.delete(id);
846
- dropped.push({
847
- name: a.name,
848
- ...a.outcome ? { outcome: a.outcome } : {},
849
- ...a.elapsedMs !== void 0 ? { elapsedMs: a.elapsedMs } : {},
850
- ...a.sessionFile ? { sessionFile: a.sessionFile } : {},
851
- ...a.content ? { copy: saveCopy(a.name, a.content) } : {}
852
- });
1465
+ for (const o of a.opts ?? []) o.triggerTurn = false;
1466
+ for (const c of a.calls) names.add(c);
1467
+ }
1468
+ const dropped = [];
1469
+ for (const c of names) {
1470
+ const a = owed.get(c);
1471
+ if (!a) continue;
1472
+ owed.delete(c);
1473
+ dropped.push(a);
853
1474
  }
1475
+ trace("dropped", { calls: dropped.map((d) => d.callId) });
854
1476
  if (dropped.length === 0) return;
855
- showStatus();
856
1477
  void Promise.resolve(
857
- pi.sendMessage(
1478
+ send(
858
1479
  {
859
1480
  customType: DELEGATE_MESSAGE_TYPE,
860
1481
  content: droppedNote(dropped),
861
1482
  display: true,
862
- details: { dropped }
1483
+ details: { dropped: dropped.map((d) => d.callId) }
863
1484
  },
864
- { triggerTurn: false }
1485
+ { triggerTurn: false },
1486
+ "dropped-note"
865
1487
  )
866
1488
  ).catch(() => {
867
1489
  });
1490
+ };
1491
+ const recoverLost = (candidates, ctx) => {
1492
+ settleCheck = void 0;
1493
+ if (closed) return;
1494
+ let idle = false;
1495
+ try {
1496
+ idle = ctx.isIdle?.() ?? true;
1497
+ } catch {
1498
+ }
1499
+ if (!idle) return;
1500
+ if ([...arriving].some(([id, a2]) => a2.sent && !candidates.includes(id))) return;
1501
+ const lost = candidates.filter((id) => arriving.get(id)?.sent);
1502
+ dropWakes(lost.filter((id) => arriving.get(id)?.resent));
1503
+ const next = lost.find((id) => arriving.has(id));
1504
+ const a = next === void 0 ? void 0 : arriving.get(next);
1505
+ if (next === void 0 || !a?.message) {
1506
+ wake("wake-after-settle");
1507
+ return;
1508
+ }
1509
+ a.resent = true;
1510
+ trace("resend", { wake: next, calls: a.calls });
1511
+ void Promise.resolve(
1512
+ send(a.message, { deliverAs: "followUp", triggerTurn: true }, "resend")
1513
+ ).catch(() => {
1514
+ arriving.delete(next);
1515
+ });
1516
+ };
1517
+ pi.on("agent_start", () => {
1518
+ busy = true;
1519
+ });
1520
+ pi.on("agent_settled", (e, ctx) => {
1521
+ busy = false;
1522
+ const sent = [...arriving].filter(([, a]) => a.sent).map(([id]) => id);
1523
+ if (e.aborted === true) {
1524
+ clearImmediate(settleCheck);
1525
+ dropWakes(sent, [...owed.keys()]);
1526
+ return;
1527
+ }
1528
+ if (sent.length === 0 && owed.size === 0) return;
1529
+ clearImmediate(settleCheck);
1530
+ settleCheck = setImmediate(() => recoverLost(sent, ctx));
868
1531
  });
869
- pi.on("session_shutdown", (_e, ctx) => {
870
- if (arriving.size > 0) {
871
- arriving.clear();
872
- showStatus();
1532
+ pi.on("session_start", async (_e, ctx) => {
1533
+ const session = piSessionId(ctx) || process.env.PI_SESSION_ID;
1534
+ if (!session || closed) return;
1535
+ const r = await mu(["delegates", "list", "--caller", `pi:${session}`, "--json"]).catch(
1536
+ () => void 0
1537
+ );
1538
+ const items = (r ? json(r)?.items : void 0) ?? [];
1539
+ for (const row of items) {
1540
+ if (row.state !== "running" || typeof row.agent !== "string" || row.agent === row.task)
1541
+ continue;
1542
+ const call2 = callInfoOf(row);
1543
+ if (call2.caller !== `pi:${session}` || inflight.has(inflightKey(call2.workstream, row.agent)))
1544
+ continue;
1545
+ await watch(call2, row.agent, 0, {
1546
+ keep: call2.request.keep === true,
1547
+ timeoutS: call2.request.timeoutS ?? DELEGATE_TIMEOUT_S
1548
+ });
873
1549
  }
1550
+ });
1551
+ pi.on("session_shutdown", async (e, ctx) => {
1552
+ closed = true;
1553
+ clearImmediate(settleCheck);
1554
+ clearImmediate(wakeSoon);
1555
+ trace("session_shutdown.state", {
1556
+ arriving: [...arriving.values()].map((a) => ({ calls: a.calls, sent: a.sent })),
1557
+ owed: [...owed.keys()],
1558
+ inflight: [...inflight.keys()]
1559
+ });
1560
+ arriving.clear();
1561
+ owed.clear();
874
1562
  if (inflight.size === 0 && queue.length === 0) return;
875
- const names = [...inflight.keys()];
1563
+ const shown = (key) => key.startsWith(`${W}/`) ? key.slice(W.length + 1) : key;
1564
+ const names = [...inflight].flatMap(([k, e2]) => e2.process ? [] : [shown(k)]);
1565
+ const procs = [...inflight].flatMap(([k, e2]) => e2.process ? [shown(k)] : []);
876
1566
  const sessions = [...inflight].flatMap(
877
- ([n, e]) => e.sessionFile ? [`${n}: ${e.sessionFile}`] : []
1567
+ ([k, e2]) => e2.sessionFile ? [`${shown(k)}: ${e2.sessionFile}`] : []
878
1568
  );
879
- const never = queue.splice(0).map((q) => q.handle);
880
- for (const e of inflight.values()) e.abort.abort();
1569
+ const dropped = queue.splice(0);
1570
+ const reason2 = e?.reason;
1571
+ const replacing = reason2 === "reload" || reason2 === "new" || reason2 === "resume" || reason2 === "fork";
1572
+ const stopping = [];
1573
+ for (const f of inflight.values())
1574
+ if (!f.process || !replacing) {
1575
+ f.abort.abort();
1576
+ const call2 = f.call;
1577
+ if (f.process)
1578
+ stopping.push(f.process.then(() => finishOrphan(call2, "cancelled", "pi quit")));
1579
+ }
881
1580
  inflight.clear();
882
- showStatus();
883
- ui = void 0;
1581
+ reportPending();
1582
+ await Promise.all(stopping);
1583
+ const refused = [];
1584
+ await Promise.all(
1585
+ dropped.map(async (q) => {
1586
+ try {
1587
+ await q.registered;
1588
+ await calls.mark(q.call.callId, "refused", {
1589
+ error: "its session ended before it started"
1590
+ });
1591
+ refused.push(q.call.callId);
1592
+ } catch {
1593
+ }
1594
+ })
1595
+ );
884
1596
  const parts = [];
885
1597
  if (names.length > 0)
886
1598
  parts.push(
887
- `${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.`
1599
+ `${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. ${reason2 === "reload" ? "This session's next runtime re-adopts them and delivers their answers." : `Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`}`
1600
+ );
1601
+ if (procs.length > 0)
1602
+ parts.push(
1603
+ replacing ? `${procs.length} mux-free delegate${procs.length === 1 ? "" : "s"} still running: ${procs.join(", ")}. Each finishes onto its task (mu task notes <name> -w ${W}); the answer will not come back here.` : `${procs.length} mux-free delegate${procs.length === 1 ? "" : "s"} cancelled: ${procs.join(", ")}.`
888
1604
  );
889
1605
  if (sessions.length > 0) parts.push(`Sessions: ${sessions.join("; ")}.`);
890
- if (never.length > 0)
891
- parts.push(`${never.length} queued, never started: ${never.join(", ")}. Re-issue them.`);
1606
+ if (dropped.length > 0)
1607
+ parts.push(
1608
+ `${dropped.length} queued, never started: ${dropped.map((q) => q.handle).join(", ")}. Re-issue them${refused.length > 0 ? ` (durable and refused: ${DELEGATE_REISSUE_TOOL} in a new pi session; list: mu delegates --all)` : ""}.`
1609
+ );
892
1610
  const msg = parts.join(" ");
893
1611
  if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
894
1612
  else process.stderr.write(`mu: ${msg}
895
1613
  `);
896
1614
  });
897
- return { answerPending: () => arriving.size > 0 };
1615
+ return {
1616
+ answerPending: () => arriving.size > 0,
1617
+ delivered(shown) {
1618
+ if (Array.isArray(shown)) for (const c of shown) owed.delete(c);
1619
+ else
1620
+ for (const [c, a] of owed) if (a.at < shown.before) owed.delete(c);
1621
+ }
1622
+ };
898
1623
  }
899
1624
 
900
1625
  // extension/nudge.ts
@@ -903,14 +1628,12 @@ import { join as join3 } from "path";
903
1628
  var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
904
1629
  var NUDGE_LOG_KIND = "nudge";
905
1630
  var CLOSE_NUDGE_MESSAGE_TYPE = "mu-close-task";
906
- var REFUTE_NUDGE_MESSAGE_TYPE = "mu-refute-brief";
907
1631
  var MARK_START = "<!-- mu:keep-driving -->";
908
1632
  var MARK_END = "<!-- /mu:keep-driving -->";
909
- var WAIT_HINT_MAX = 8;
910
1633
  var notNested = () => false;
911
1634
  var noHold = () => void 0;
912
1635
  function withQueuedInput(hold) {
913
- return (ctx) => ctx?.hasPendingMessages?.() ? "answer" : hold(ctx);
1636
+ return (ctx) => ctx?.hasPendingMessages?.() ? "input" : hold(ctx);
914
1637
  }
915
1638
  function noUI(ctx) {
916
1639
  return ctx?.hasUI === false;
@@ -932,190 +1655,13 @@ function readSkill() {
932
1655
  }
933
1656
  return void 0;
934
1657
  }
935
- var ENV_ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=/;
936
- function muArgs(words) {
937
- let at = 0;
938
- while (at < words.length && ENV_ASSIGN.test(words[at] ?? "")) at++;
939
- const cmd = words[at];
940
- if (cmd !== "mu" && !cmd?.endsWith("/mu")) return void 0;
941
- return words.slice(at + 1);
942
- }
943
- function dispatchedWorkstreams(command) {
944
- const found = /* @__PURE__ */ new Set();
945
- for (const words of shellSegments(command)) {
946
- const args = muArgs(words);
947
- if (args === void 0) continue;
948
- const claim = claimOf(args);
949
- if (claim !== void 0) {
950
- if (claim.ws !== "scratch") found.add(claim.ws);
951
- continue;
952
- }
953
- if (args[0] !== "agent" || args[1] !== "send" && args[1] !== "spawn") continue;
954
- let ws = "";
955
- for (let k = 2; k < args.length; k++) {
956
- const w = args[k] ?? "";
957
- if (w === "-w" || w === "--workstream") ws = args[k + 1] ?? "";
958
- else if (w.startsWith("--workstream=")) ws = w.slice("--workstream=".length);
959
- }
960
- if (ws !== "scratch") found.add(ws);
961
- }
962
- return found.size > 0 ? [...found] : void 0;
963
- }
964
- var CLAIM_VALUE_OPTS = /* @__PURE__ */ new Set(["--for", "-f", "-w", "--workstream", "--evidence", "--actor"]);
965
- function shellSegments(command) {
966
- const segs = [];
967
- let words = [];
968
- let word;
969
- const heredocs = [];
970
- let arithEnd = -1;
971
- const endWord = () => {
972
- if (word !== void 0) words.push(word);
973
- word = void 0;
974
- };
975
- const endSeg = () => {
976
- endWord();
977
- if (words.length > 0) segs.push(words);
978
- words = [];
979
- };
980
- const skipHeredocs = (i) => {
981
- for (const h of heredocs.splice(0)) {
982
- while (i < command.length) {
983
- const eol = command.indexOf("\n", i + 1);
984
- const end = eol < 0 ? command.length : eol;
985
- const line = command.slice(i + 1, end);
986
- i = end;
987
- if ((h.strip ? line.replace(/^\t+/, "") : line) === h.delim) break;
988
- }
989
- }
990
- return i;
991
- };
992
- for (let i = 0; i < command.length; i++) {
993
- const c = command[i] ?? "";
994
- if (c === "'" || c === '"') {
995
- const close = command.indexOf(c, i + 1);
996
- const end = close < 0 ? command.length : close;
997
- word = (word ?? "") + command.slice(i + 1, end);
998
- i = end;
999
- } else if (c === "\\" && command[i + 1] === "\n") {
1000
- i++;
1001
- } else if (c === "\\" && i + 1 < command.length) {
1002
- word = (word ?? "") + command[++i];
1003
- } else if (c === "#" && word === void 0) {
1004
- const eol = command.indexOf("\n", i);
1005
- i = (eol < 0 ? command.length : eol) - 1;
1006
- } else if (c === "(" && command[i + 1] === "(" && i > arithEnd) {
1007
- arithEnd = arithmeticEnd(command, i);
1008
- word = `${word ?? ""}((`;
1009
- i++;
1010
- } else if (command.startsWith("<<<", i)) {
1011
- endWord();
1012
- i += 2;
1013
- } else if (c === "<" && command[i + 1] === "<" && i > arithEnd) {
1014
- endWord();
1015
- const m = /^<<(-?)[ \t]*('([^']*)'|"([^"]*)"|[^\s;&|<>()]+)/.exec(command.slice(i));
1016
- if (!m) {
1017
- i++;
1018
- continue;
1019
- }
1020
- const delim = m[3] ?? m[4] ?? (m[2] ?? "").replace(/\\/g, "");
1021
- heredocs.push({ delim, strip: m[1] === "-" });
1022
- i += m[0].length - 1;
1023
- } else if (c === ";" || c === "|" || c === "\n" || c === "&" && command[i + 1] === "&") {
1024
- if (c !== ";" && c !== "\n" && command[i + 1] === c) i++;
1025
- endSeg();
1026
- if (c === "\n" && heredocs.length > 0) i = skipHeredocs(i);
1027
- } else if (/\s/.test(c)) {
1028
- endWord();
1029
- } else {
1030
- word = (word ?? "") + c;
1031
- }
1032
- }
1033
- endSeg();
1034
- return segs;
1035
- }
1036
- function arithmeticEnd(command, open) {
1037
- let depth = 0;
1038
- for (let i = open + 2; i < command.length; i++) {
1039
- const c = command[i];
1040
- if (c === "'" || c === '"') {
1041
- const close = command.indexOf(c, i + 1);
1042
- if (close < 0) return -1;
1043
- i = close;
1044
- } else if (c === "\\") {
1045
- i++;
1046
- } else if (c === "(") {
1047
- depth++;
1048
- } else if (c === ")") {
1049
- if (depth > 0) depth--;
1050
- else return command[i + 1] === ")" ? i + 1 : -1;
1051
- }
1052
- }
1053
- return -1;
1054
- }
1055
- function claimOf(args) {
1056
- if (args[0] !== "task" || args[1] !== "claim") return void 0;
1057
- let ws = "";
1058
- let id;
1059
- let isFor = false;
1060
- for (let k = 2; k < args.length; k++) {
1061
- const w = args[k] ?? "";
1062
- const [opt, eq] = w.startsWith("--") && w.includes("=") ? w.split("=", 2) : [w, void 0];
1063
- if (opt === "--for" || opt === "-f") isFor = true;
1064
- if (opt === "-w" || opt === "--workstream") ws = eq ?? args[k + 1] ?? "";
1065
- if (CLAIM_VALUE_OPTS.has(opt ?? "")) {
1066
- if (eq === void 0) k++;
1067
- } else if (id === void 0 && !w.startsWith("-")) id = w;
1068
- }
1069
- if (!isFor || id === void 0) return void 0;
1070
- const slash = id.indexOf("/");
1071
- if (slash >= 0) {
1072
- if (ws === "") ws = id.slice(0, slash);
1073
- id = id.slice(slash + 1);
1074
- }
1075
- return { ws, id };
1076
- }
1077
- function dispatchedTasks(command) {
1078
- const out = [];
1079
- for (const words of shellSegments(command)) {
1080
- const args = muArgs(words);
1081
- const claim = args === void 0 ? void 0 : claimOf(args);
1082
- if (claim !== void 0 && claim.ws !== "scratch") out.push(claim);
1083
- }
1084
- return out;
1085
- }
1086
- async function inProgress(run, workstreams) {
1087
- const results = await Promise.all(
1088
- workstreams.map(async (w) => {
1089
- const r = await run([
1090
- "task",
1091
- "list",
1092
- "--status",
1093
- "IN_PROGRESS",
1094
- "--json",
1095
- "--sort",
1096
- "recency",
1097
- ...w ? ["-w", w] : []
1098
- ]);
1099
- if (r.code !== 0) return void 0;
1100
- try {
1101
- const items = JSON.parse(r.stdout).items ?? [];
1102
- return items.flatMap((t) => t.name ? [`${t.workstreamName ?? w}/${t.name}`] : []);
1103
- } catch {
1104
- return void 0;
1105
- }
1106
- })
1107
- );
1108
- if (results.some((r) => r === void 0)) return void 0;
1109
- return [...new Set(results.flatMap((r) => r ?? []))];
1110
- }
1111
- function nudgeText(rule, refs) {
1112
- const shown = refs.slice(0, WAIT_HINT_MAX).join(" ");
1113
- const more = refs.length > WAIT_HINT_MAX ? ` (+${refs.length - WAIT_HINT_MAX} more)` : "";
1114
- return [
1115
- `[mu] ${rule}`,
1116
- "",
1117
- `${refs.length} task(s) IN_PROGRESS. Next: mu task wait ${shown} --first --on-stall exit --json${more}`
1118
- ].join("\n");
1658
+ async function allItems(run, argv, uncapped) {
1659
+ const r = await run([...argv, uncapped]);
1660
+ if (r.code !== 0) return void 0;
1661
+ const data = parseEnvelope(r.stdout);
1662
+ if (data === void 0 || !Array.isArray(data.items)) return void 0;
1663
+ if ((data.more ?? 0) > 0) return void 0;
1664
+ return data.items;
1119
1665
  }
1120
1666
  function breadcrumb(run, args) {
1121
1667
  void run(args).catch(() => {
@@ -1149,7 +1695,8 @@ function parallelSettle(pi, hold = noHold) {
1149
1695
  const late = hold(ctx);
1150
1696
  const fired = results.flatMap((r) => {
1151
1697
  if (r === void 0) return [];
1152
- if (late === "answer" || late === "blocked" && r.continue === true) {
1698
+ if (r.receipt === true && late !== void 0) return [{ entries: r.entries ?? [] }];
1699
+ if (late === "input" || late === "wake" || late === "blocked" && r.continue === true) {
1153
1700
  r.yielded?.();
1154
1701
  return [];
1155
1702
  }
@@ -1176,54 +1723,294 @@ function parallelSettle(pi, hold = noHold) {
1176
1723
  function outcomeOf(event) {
1177
1724
  return typeof event === "object" && event !== null ? event.outcome : void 0;
1178
1725
  }
1179
- function bashCommand(event) {
1180
- if (typeof event !== "object" || event === null) return void 0;
1181
- const e = event;
1182
- return e.toolName === "bash" && typeof e.input?.command === "string" ? e.input.command : void 0;
1726
+ var SETTLE_DRAIN_MS = 3e3;
1727
+ var SETTLE_WAIT_ARGS = ["wait", "--first", "--on-stall", "exit", "--json"];
1728
+ var SETTLE_READY_ARGS = [
1729
+ "wait",
1730
+ "--first",
1731
+ "--on-stall",
1732
+ "exit",
1733
+ "--timeout",
1734
+ "1",
1735
+ "--json"
1736
+ ];
1737
+ var READY_MAX = 50;
1738
+ var ESC = /^\u001b(\[27(;\d+)*u)?$/;
1739
+ function envelopeOf(stdout) {
1740
+ try {
1741
+ const v = JSON.parse(stdout);
1742
+ return typeof v === "object" && v !== null && typeof v.ok === "boolean" ? v : void 0;
1743
+ } catch {
1744
+ return void 0;
1745
+ }
1746
+ }
1747
+ function sessionIdOf(ctx) {
1748
+ try {
1749
+ return ctx?.sessionManager?.getSessionId?.() || void 0;
1750
+ } catch {
1751
+ return void 0;
1752
+ }
1753
+ }
1754
+ function identityEnvs(sid) {
1755
+ const e = process.env;
1756
+ const out = [];
1757
+ if (e.MU_MANAGED_AGENT === "1" && e.MU_WORKSTREAM && e.MU_AGENT_NAME)
1758
+ out.push({ PI_SESSION_ID: void 0 });
1759
+ if (sid !== void 0) out.push({ MU_MANAGED_AGENT: void 0, PI_SESSION_ID: sid });
1760
+ return out;
1761
+ }
1762
+ async function humanNeeds(run, sid) {
1763
+ const counts = await Promise.all(
1764
+ identityEnvs(sid).map(async (env) => {
1765
+ const r = await run(["state", "--me", "--all", "--full", "--json"], void 0, void 0, {
1766
+ env
1767
+ }).catch(() => void 0);
1768
+ const env0 = r === void 0 ? void 0 : envelopeOf(r.stdout);
1769
+ const ws = env0?.ok === true ? env0.data?.workstreams : void 0;
1770
+ if (!Array.isArray(ws)) return void 0;
1771
+ return ws.reduce(
1772
+ (n, w) => n + (w.human_needs?.length ?? 0),
1773
+ 0
1774
+ );
1775
+ })
1776
+ );
1777
+ return counts.some((c) => c === void 0) ? void 0 : counts.reduce((a, b) => a + (b ?? 0), 0);
1778
+ }
1779
+ function entry(content) {
1780
+ return { type: "custom_message", customType: NUDGE_MESSAGE_TYPE, content, display: true };
1781
+ }
1782
+ var REPORT_CAP = 4e3;
1783
+ var WAVE_REPORT_CAP = 1500;
1784
+ var PENDING_STATES = /* @__PURE__ */ new Set(["queued", "starting", "running", "recording"]);
1785
+ var recordOf = (c) => c.record?.workstream && c.record.task ? `${c.record.workstream}/${c.record.task}` : void 0;
1786
+ async function reportOf(mu, c) {
1787
+ const v = await mu(["task", "notes", c.task, "-w", c.workstream, "--json"]);
1788
+ const items = v?.ok === true ? v.data?.items : void 0;
1789
+ return items?.filter((n) => n.kind === "report").at(-1)?.content;
1790
+ }
1791
+ async function answerBlock(mu, c, cap) {
1792
+ const where = recordOf(c);
1793
+ const notes = `mu task notes ${c.task} -w ${c.workstream}`;
1794
+ const head = [
1795
+ `Delegate answer: call ${c.callId}, task ${c.workstream}/${c.task}, ${c.state}`,
1796
+ c.verdict ? `, ${c.verdict}` : "",
1797
+ where ? `, recorded on ${where}` : "",
1798
+ c.error ? `. Error: ${c.error}` : "",
1799
+ "."
1800
+ ].join("");
1801
+ if (where && c.verdict) return `${head} Full answer: ${notes}`;
1802
+ const text = await reportOf(mu, c);
1803
+ if (text === void 0) return `${head} Report unreadable here; read it: ${notes}`;
1804
+ const body = text.length > cap ? `${text.slice(0, cap)}
1805
+ [... ${text.length - cap} more characters: ${notes}]` : text;
1806
+ return `${head}
1807
+ ${body}`;
1808
+ }
1809
+ function waveSummary(wave, rows) {
1810
+ const n = (p) => rows.filter(p).length;
1811
+ const refused = rows.filter((c) => c.state === "refused");
1812
+ const counts = [
1813
+ `${n((c) => c.verdict === "CONFIRMED")}/${rows.length} CONFIRMED`,
1814
+ `${n((c) => c.verdict === "REFUTED")} REFUTED`,
1815
+ `${n((c) => c.state !== "refused" && !PENDING_STATES.has(c.state) && c.verdict !== "CONFIRMED" && c.verdict !== "REFUTED")} UNVERIFIED`,
1816
+ `${n((c) => PENDING_STATES.has(c.state))} pending`,
1817
+ `${refused.length} refused`
1818
+ ].join(", ");
1819
+ const lines = rows.map(
1820
+ (c) => `- ${recordOf(c) ?? `${c.workstream}/${c.task}`}: ${c.state === "refused" ? "REFUSED, reissue it: mu_delegate_reissue" : c.verdict ?? c.state} (call ${c.callId})`
1821
+ );
1822
+ return [`Wave ${wave}: ${counts}.`, ...lines].join("\n");
1823
+ }
1824
+ function firingDelegate(d) {
1825
+ const f = d.firing;
1826
+ if (f?.kind !== "delegate" || typeof f.ref !== "string") return void 0;
1827
+ return f.ref.startsWith("delegate:") ? f.ref.slice("delegate:".length) : void 0;
1828
+ }
1829
+ function pictureOf(d) {
1830
+ if (typeof d.snapshotsCut === "number" && d.snapshotsCut > 0) return void 0;
1831
+ const snaps = Array.isArray(d.snapshots) ? d.snapshots : [];
1832
+ return new Set(
1833
+ snaps.flatMap(
1834
+ (s) => typeof s.ref === "string" && s.ref.startsWith("delegate:") ? [s.ref.slice("delegate:".length)] : []
1835
+ )
1836
+ );
1183
1837
  }
1184
- function registerNudge(pi, run = defaultRunner(), skillMd = readSkill(), nested = notNested, hold = noHold) {
1838
+ var noDelivered = () => {
1839
+ };
1840
+ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill(), nested = notNested, hold = noHold, delivered = noDelivered) {
1185
1841
  if (!nudgeEnabled()) return;
1842
+ if (process.env.MU_REMOTE === "1") return;
1186
1843
  const rule = skillMd === void 0 ? void 0 : keepDrivingRule(skillMd);
1187
1844
  if (rule === void 0) return;
1188
- const armed = /* @__PURE__ */ new Set();
1189
- let fired = false;
1190
- pi.on("input", () => {
1191
- armed.clear();
1192
- fired = false;
1193
- });
1194
- pi.on("tool_call", (event) => {
1195
- const cmd = bashCommand(event);
1196
- if (cmd === void 0) return;
1197
- for (const ws of dispatchedWorkstreams(cmd) ?? []) armed.add(ws);
1198
- });
1845
+ let current;
1846
+ const cancel = () => current?.abort();
1847
+ pi.on("input", cancel);
1848
+ pi.on("ui_prompt_start", cancel);
1849
+ pi.on("session_shutdown", cancel);
1199
1850
  pi.on("agent_before_settle", async (event, ctx) => {
1200
- if (fired || armed.size === 0 || nested() || hold(ctx)) return;
1851
+ if (nested() || hold(ctx)) return;
1201
1852
  if (outcomeOf(event) !== "completed") return;
1202
- fired = true;
1203
- const refs = await inProgress(run, [...armed]);
1204
- if (refs === void 0 || refs.length === 0) return;
1205
- if (hold(ctx)) {
1206
- fired = false;
1207
- return;
1208
- }
1209
- const text = nudgeText(rule, refs);
1210
- const logWs = [...armed].find((w) => w !== "");
1211
- breadcrumb(run, [
1212
- "log",
1213
- ...logWs ? ["-w", logWs] : [],
1214
- "--kind",
1215
- NUDGE_LOG_KIND,
1216
- `keep-driving: ${refs.length} in progress`
1217
- ]);
1218
- return {
1219
- entries: [
1220
- { type: "custom_message", customType: NUDGE_MESSAGE_TYPE, content: text, display: true }
1221
- ],
1222
- continue: true,
1223
- yielded: () => {
1224
- fired = false;
1853
+ const sid = sessionIdOf(ctx);
1854
+ if (identityEnvs(sid).length === 0) return;
1855
+ const env = { PI_SESSION_ID: sid };
1856
+ const ctl = new AbortController();
1857
+ current = ctl;
1858
+ const off = ctx?.ui?.onTerminalInput?.((data) => {
1859
+ if (ESC.test(data)) ctl.abort();
1860
+ return void 0;
1861
+ }) ?? (() => {
1862
+ });
1863
+ const wait = (args) => run(args, ctl.signal, void 0, { env, drainMs: SETTLE_DRAIN_MS });
1864
+ const mu = async (args) => envelopeOf(
1865
+ (await run(args, void 0, void 0, { env }).catch(() => void 0))?.stdout ?? ""
1866
+ );
1867
+ const call2 = async (id) => {
1868
+ const v = await mu(["delegates", "show", id, "--json"]);
1869
+ return v?.ok === true ? v.data?.call : void 0;
1870
+ };
1871
+ const answers = async (callId, shown, first) => {
1872
+ shown.add(callId);
1873
+ const c = await call2(callId);
1874
+ if (c === void 0)
1875
+ return {
1876
+ blocks: [
1877
+ `Delegate answer: call ${callId}. Its row is unreadable here: mu delegates show ${callId}`
1878
+ ],
1879
+ stop: false
1880
+ };
1881
+ const wave = c.wave;
1882
+ if (!wave) return { blocks: [await answerBlock(mu, c, REPORT_CAP)], stop: false };
1883
+ const listed = async () => {
1884
+ const v = await mu(["delegates", "list", "--wave", wave, "--json"]);
1885
+ const items = v?.ok === true ? v.data?.items : void 0;
1886
+ return Array.isArray(items) ? items : [c];
1887
+ };
1888
+ const after = [];
1889
+ let stop = false;
1890
+ let rows = await listed();
1891
+ let picture = pictureOf(first);
1892
+ const owedRow = (r) => PENDING_STATES.has(r.state) || r.state !== "refused" && !shown.has(r.callId) && picture?.has(r.callId) !== false;
1893
+ while (!ctl.signal.aborted && rows.some(owedRow)) {
1894
+ const r = await wait(SETTLE_WAIT_ARGS);
1895
+ const v = envelopeOf(r.stdout);
1896
+ if (v?.ok === false && v.error === "WaitNothingError") break;
1897
+ const d = v?.ok === true ? v.data : void 0;
1898
+ if (d === void 0) {
1899
+ after.push(
1900
+ `The wave's wait failed (exit ${r.code}): ${(r.stdout.trim() || r.stderr.trim()).slice(0, 500)}`
1901
+ );
1902
+ break;
1903
+ }
1904
+ if (d.interrupted === true || d.drained === true) break;
1905
+ picture = pictureOf(d);
1906
+ if (d.timedOut === true) continue;
1907
+ if (Array.isArray(d.humanNeeds) && d.humanNeeds.length > 0) stop = true;
1908
+ const id = firingDelegate(d);
1909
+ const sibling = id !== void 0 && rows.some((x) => x.callId === id);
1910
+ if (id !== void 0) shown.add(id);
1911
+ if (sibling && !stop) {
1912
+ rows = await listed();
1913
+ continue;
1914
+ }
1915
+ after.push(`Another event came before the rest of wave ${wave}:
1916
+ ${JSON.stringify(d)}`);
1917
+ const oc = id === void 0 || sibling ? void 0 : await call2(id);
1918
+ if (oc) after.push(await answerBlock(mu, oc, REPORT_CAP));
1919
+ break;
1920
+ }
1921
+ rows = await listed();
1922
+ const bodies = await Promise.all(
1923
+ rows.filter((x) => shown.has(x.callId)).map((x) => answerBlock(mu, x, WAVE_REPORT_CAP))
1924
+ );
1925
+ return { blocks: [waveSummary(wave, rows), ...bodies, ...after], stop };
1926
+ };
1927
+ const ready = async () => {
1928
+ const blocks = [];
1929
+ const shown = [];
1930
+ for (let i = 0; i < READY_MAX && !ctl.signal.aborted; i++) {
1931
+ const started = Date.now();
1932
+ const r = await wait(SETTLE_READY_ARGS);
1933
+ const v = envelopeOf(r.stdout);
1934
+ if (v?.ok === false && v.error === "WaitNothingError") {
1935
+ delivered({ before: started });
1936
+ break;
1937
+ }
1938
+ const d = v?.ok === true ? v.data : void 0;
1939
+ if (d === void 0) {
1940
+ const why = (r.stdout.trim() || r.stderr.trim()).slice(0, 2e3);
1941
+ blocks.push(`mu wait failed (exit ${r.code}): ${why}`);
1942
+ break;
1943
+ }
1944
+ if (d.drained === true) delivered({ before: started });
1945
+ if (d.interrupted === true || d.drained === true || d.timedOut === true) break;
1946
+ blocks.push(JSON.stringify(d));
1947
+ const id = firingDelegate(d);
1948
+ if (id === void 0) continue;
1949
+ shown.push(id);
1950
+ const c = await call2(id);
1951
+ blocks.push(
1952
+ c === void 0 ? `Delegate answer: call ${id}. Its row is unreadable here: mu delegates show ${id}` : await answerBlock(mu, c, REPORT_CAP)
1953
+ );
1225
1954
  }
1955
+ if (shown.length > 0) delivered(shown);
1956
+ if (blocks.length === 0) return void 0;
1957
+ const text = [
1958
+ "[mu] A human stop is open (mu human-needed), so the settle did not wait. It showed what was already ready:",
1959
+ ...blocks.flatMap((b) => ["", b])
1960
+ ].join("\n");
1961
+ return { entries: [entry(text)], receipt: true };
1226
1962
  };
1963
+ let result;
1964
+ try {
1965
+ result = await (async () => {
1966
+ for (; ; ) {
1967
+ const needs = await humanNeeds(run, sid);
1968
+ if (ctl.signal.aborted || hold(ctx)) return void 0;
1969
+ if (needs === void 0)
1970
+ return {
1971
+ entries: [
1972
+ entry(
1973
+ "[mu] settle: could not read your open human needs (mu state --me); not waiting."
1974
+ )
1975
+ ]
1976
+ };
1977
+ if (needs > 0) return ready();
1978
+ const started = Date.now();
1979
+ const r = await wait(SETTLE_WAIT_ARGS);
1980
+ const v = envelopeOf(r.stdout);
1981
+ if (v?.ok === false && v.error === "WaitNothingError") {
1982
+ delivered({ before: started });
1983
+ return void 0;
1984
+ }
1985
+ if (v === void 0 || v.ok !== true || v.data === void 0) {
1986
+ const why = (r.stdout.trim() || r.stderr.trim()).slice(0, 2e3);
1987
+ return { entries: [entry(`[mu] settle: mu wait failed (exit ${r.code}): ${why}`)] };
1988
+ }
1989
+ const d = v.data;
1990
+ if (d.drained === true) delivered({ before: started });
1991
+ if (d.interrupted === true || d.drained === true) return void 0;
1992
+ if (d.timedOut === true) continue;
1993
+ const callId = firingDelegate(d);
1994
+ const shown = /* @__PURE__ */ new Set();
1995
+ const got = callId === void 0 ? void 0 : await answers(callId, shown, d);
1996
+ const blocks = got?.blocks ?? [];
1997
+ if (shown.size > 0) delivered([...shown]);
1998
+ const stop = Array.isArray(d.humanNeeds) && d.humanNeeds.length > 0 || got?.stop === true;
1999
+ const text = [
2000
+ `[mu] ${rule}`,
2001
+ "",
2002
+ `Work you own is still running, so the settle ran \`mu ${SETTLE_WAIT_ARGS.join(" ")}\` for you. Its result:`,
2003
+ JSON.stringify(d),
2004
+ ...blocks.flatMap((b) => ["", b])
2005
+ ].join("\n");
2006
+ return { entries: [entry(text)], ...stop ? {} : { continue: true }, receipt: true };
2007
+ }
2008
+ })();
2009
+ } finally {
2010
+ off();
2011
+ if (current === ctl) current = void 0;
2012
+ }
2013
+ return result;
1227
2014
  });
1228
2015
  }
1229
2016
  function workerIdentity(env = process.env) {
@@ -1233,12 +2020,15 @@ function workerIdentity(env = process.env) {
1233
2020
  return { agent, workstream };
1234
2021
  }
1235
2022
  async function ownedInProgress(run, agent, workstream) {
1236
- const r = await run(["task", "owned-by", agent, "-w", workstream, "--json"]);
1237
- if (r.code !== 0) return void 0;
1238
2023
  try {
1239
- const json = JSON.parse(r.stdout);
1240
- return (json.items ?? []).flatMap(
1241
- (t) => t.status === "IN_PROGRESS" && t.name ? [t.name] : []
2024
+ const items = await allItems(
2025
+ run,
2026
+ ["task", "owned-by", agent, "-w", workstream, "--json"],
2027
+ "--full"
2028
+ );
2029
+ if (items === void 0) return void 0;
2030
+ return items.flatMap(
2031
+ (t) => t.status === "IN_PROGRESS" && t.kind !== "delegate" && t.id ? [t.id] : []
1242
2032
  );
1243
2033
  } catch {
1244
2034
  return void 0;
@@ -1255,6 +2045,7 @@ function closeNudgeText(workstream, names) {
1255
2045
  }
1256
2046
  function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested = notNested, hold = noHold) {
1257
2047
  if (!nudgeEnabled(env)) return;
2048
+ if (env.MU_REMOTE === "1") return;
1258
2049
  const me = workerIdentity(env);
1259
2050
  if (me === void 0) return;
1260
2051
  let fired = false;
@@ -1295,74 +2086,9 @@ function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested
1295
2086
  };
1296
2087
  });
1297
2088
  }
1298
- var REFUTE_MARK = /^(REFUTER |VERDICT:|REFUTE-EXEMPT:)/m;
1299
- async function briefRefuted(run, ws, id) {
1300
- const r = await run(["task", "notes", id, ...ws ? ["-w", ws] : [], "--json"]);
1301
- if (r.code !== 0) return void 0;
1302
- try {
1303
- const json = JSON.parse(r.stdout);
1304
- return (json.items ?? []).some((n) => REFUTE_MARK.test(n.content ?? ""));
1305
- } catch {
1306
- return void 0;
1307
- }
1308
- }
1309
- function refuteNudgeText(refs) {
1310
- return `[mu] dispatched ${refs.join(", ")} with an unrefuted brief: refute it (delegate, record) or note REFUTE-EXEMPT: <why>`;
1311
- }
1312
- function registerRefuteNudge(pi, run = defaultRunner(), nested = notNested, hold = noHold) {
1313
- if (!nudgeEnabled()) return;
1314
- const answerDue = (ctx) => hold(ctx) === "answer";
1315
- const armed = /* @__PURE__ */ new Map();
1316
- let fired = false;
1317
- pi.on("input", () => {
1318
- armed.clear();
1319
- fired = false;
1320
- });
1321
- pi.on("tool_call", (event) => {
1322
- const cmd = bashCommand(event);
1323
- if (cmd === void 0) return;
1324
- for (const t of dispatchedTasks(cmd)) {
1325
- if (!t.id.startsWith("review_")) armed.set(`${t.ws}/${t.id}`, t);
1326
- }
1327
- });
1328
- pi.on("agent_before_settle", async (event, ctx) => {
1329
- if (fired || armed.size === 0 || nested() || noUI(ctx) || answerDue(ctx)) return;
1330
- if (outcomeOf(event) !== "completed") return;
1331
- fired = true;
1332
- const tasks = [...armed];
1333
- const refuted = await Promise.all(tasks.map(([, t]) => briefRefuted(run, t.ws, t.id)));
1334
- const refs = tasks.flatMap(([ref], i) => refuted[i] === false ? [ref] : []);
1335
- if (refs.length === 0) return;
1336
- if (answerDue(ctx)) {
1337
- fired = false;
1338
- return;
1339
- }
1340
- const logWs = [...armed.values()].find((t) => t.ws !== "")?.ws;
1341
- breadcrumb(run, [
1342
- "log",
1343
- ...logWs ? ["-w", logWs] : [],
1344
- "--kind",
1345
- NUDGE_LOG_KIND,
1346
- `refute: ${refs.join(", ")}`
1347
- ]);
1348
- return {
1349
- entries: [
1350
- {
1351
- type: "custom_message",
1352
- customType: REFUTE_NUDGE_MESSAGE_TYPE,
1353
- content: refuteNudgeText(refs),
1354
- display: true
1355
- }
1356
- ],
1357
- yielded: () => {
1358
- fired = false;
1359
- }
1360
- };
1361
- });
1362
- }
1363
2089
 
1364
2090
  // extension/mu-pi.ts
1365
- var EXT_VERSION = true ? "3.13.3" : void 0;
2091
+ var EXT_VERSION = true ? "4.0.0" : void 0;
1366
2092
  var FRESH_COMMAND = "mu-fresh";
1367
2093
  var FRESH_TIMEOUT_MS = 3e4;
1368
2094
  function commandName(name) {
@@ -1378,9 +2104,9 @@ function fail(error) {
1378
2104
  }
1379
2105
  function detectPiVersion() {
1380
2106
  try {
1381
- const entry = process.argv[1];
1382
- if (!entry) return void 0;
1383
- let dir = dirname(realpathSync(entry));
2107
+ const entry2 = process.argv[1];
2108
+ if (!entry2) return void 0;
2109
+ let dir = dirname(realpathSync(entry2));
1384
2110
  for (let i = 0; i < 6; i++) {
1385
2111
  const pkgPath = join4(dir, "package.json");
1386
2112
  if (existsSync3(pkgPath)) {
@@ -1416,22 +2142,22 @@ function sharedMap() {
1416
2142
  }
1417
2143
  function lastAssistantText(event) {
1418
2144
  const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
1419
- if (!Array.isArray(msgs)) return "";
2145
+ if (!Array.isArray(msgs)) return { text: "", truncated: false };
1420
2146
  for (let i = msgs.length - 1; i >= 0; i--) {
1421
2147
  const m = msgs[i];
1422
2148
  if (typeof m !== "object" || m === null) continue;
1423
2149
  const { role, content } = m;
1424
2150
  if (role !== "assistant") continue;
1425
- if (typeof content === "string") return capText(content);
1426
- if (!Array.isArray(content)) return "";
2151
+ if (typeof content === "string") return capTail(content);
2152
+ if (!Array.isArray(content)) return { text: "", truncated: false };
1427
2153
  const parts = content.flatMap((c) => {
1428
2154
  if (typeof c !== "object" || c === null) return [];
1429
2155
  const { type, text } = c;
1430
2156
  return type === "text" && typeof text === "string" ? [text] : [];
1431
2157
  });
1432
- return capText(parts.join(""));
2158
+ return capTail(parts.join(""));
1433
2159
  }
1434
- return "";
2160
+ return { text: "", truncated: false };
1435
2161
  }
1436
2162
  function lastAssistantError(event) {
1437
2163
  const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
@@ -1446,6 +2172,13 @@ function lastAssistantError(event) {
1446
2172
  }
1447
2173
  return void 0;
1448
2174
  }
2175
+ function capTail(text) {
2176
+ if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return { text, truncated: false };
2177
+ const buf = Buffer.from(text, "utf8");
2178
+ const tail = buf.subarray(buf.length - LAST_TEXT_MAX_BYTES).toString("utf8").replace(/^\uFFFD/, "");
2179
+ return { text: `${LAST_TEXT_TRUNCATED}
2180
+ ${tail}`, truncated: true };
2181
+ }
1449
2182
  function capText(text) {
1450
2183
  if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return text;
1451
2184
  const cut = Buffer.from(text, "utf8").subarray(0, LAST_TEXT_MAX_BYTES).toString("utf8");
@@ -1524,38 +2257,15 @@ function reasonOf(event) {
1524
2257
  const r = typeof event === "object" && event !== null ? event.reason : void 0;
1525
2258
  return typeof r === "string" ? r : void 0;
1526
2259
  }
1527
- function promptsDir(from = import.meta.url) {
1528
- let here;
1529
- try {
1530
- here = dirname(fileURLToPath2(from));
1531
- } catch {
1532
- return void 0;
1533
- }
1534
- for (const up of ["..", "../.."]) {
1535
- const d = join4(here, up, "prompts");
1536
- if (existsSync3(join4(d, "ultrathink.md"))) return d;
1537
- }
1538
- return void 0;
1539
- }
1540
- function registerPrompts(pi) {
1541
- pi.on("resources_discover", () => {
1542
- const d = promptsDir();
1543
- if (!d) return {};
1544
- const paths = readdirSync(d).filter((f) => f.endsWith(".md")).sort().map((f) => join4(d, f));
1545
- return { promptPaths: paths };
1546
- });
1547
- }
1548
2260
  function muPi(pi, run = defaultRunner()) {
1549
2261
  const delegates = registerDelegate(pi, run);
1550
- registerPrompts(pi);
1551
2262
  const nested = serveCtl(pi);
1552
2263
  const hold = withQueuedInput(
1553
- () => delegates.answerPending() ? "answer" : isUiBlocked() ? "blocked" : void 0
2264
+ () => delegates.answerPending() ? "wake" : isUiBlocked() ? "blocked" : void 0
1554
2265
  );
1555
2266
  const settle = parallelSettle(pi, hold);
1556
- registerNudge(settle.api, run, void 0, nested, hold);
2267
+ registerNudge(settle.api, run, void 0, nested, hold, delegates.delivered);
1557
2268
  registerCloseNudge(settle.api, run, process.env, nested, hold);
1558
- registerRefuteNudge(settle.api, run, nested, hold);
1559
2269
  settle.flush();
1560
2270
  }
1561
2271
  function serveCtl(pi) {
@@ -1570,25 +2280,42 @@ function serveCtl(pi) {
1570
2280
  const existing = map.get(sockPath);
1571
2281
  const g = existing ?? {
1572
2282
  conns: /* @__PURE__ */ new Set(),
2283
+ incarnation: randomUUID(),
2284
+ accepted: /* @__PURE__ */ new Map(),
1573
2285
  waiters: /* @__PURE__ */ new Set(),
1574
2286
  state: "idle",
1575
2287
  since: Date.now(),
1576
2288
  runs: 0,
2289
+ tools: /* @__PURE__ */ new Map(),
1577
2290
  lastText: "",
1578
2291
  pi,
1579
2292
  handle
1580
2293
  };
1581
2294
  g.pi = pi;
1582
2295
  g.handle = handle;
2296
+ g.incarnation ??= randomUUID();
2297
+ g.accepted ??= /* @__PURE__ */ new Map();
2298
+ g.tools ??= /* @__PURE__ */ new Map();
1583
2299
  map.set(sockPath, g);
1584
2300
  const status = () => ({
2301
+ incarnation: g.incarnation,
1585
2302
  state: g.state,
1586
2303
  since: g.since,
1587
2304
  runs: g.runs,
1588
2305
  pending: g.ctx?.hasPendingMessages() ?? false,
2306
+ tool: [...g.tools.values()].at(-1) ?? null,
1589
2307
  ...sessionFileOf(g.ctx),
1590
2308
  ...blockedField()
1591
2309
  });
2310
+ const remember = (id, runs) => {
2311
+ if (id === void 0) return;
2312
+ g.accepted.delete(id);
2313
+ g.accepted.set(id, { runs, at: Date.now() });
2314
+ for (const old of g.accepted.keys()) {
2315
+ if (g.accepted.size <= CTL_MSGID_MEMORY) break;
2316
+ g.accepted.delete(old);
2317
+ }
2318
+ };
1592
2319
  const blockedField = () => {
1593
2320
  const b = uiBlocked();
1594
2321
  return b ? { blocked: b } : {};
@@ -1614,8 +2341,20 @@ function serveCtl(pi) {
1614
2341
  g.since = Date.now();
1615
2342
  if (g.fresh?.armed) settleFresh({ v: V, ok: true, ...status() });
1616
2343
  });
2344
+ pi.on("tool_execution_start", (e) => {
2345
+ const { toolCallId, toolName } = e ?? {};
2346
+ if (typeof toolCallId === "string" && typeof toolName === "string") {
2347
+ g.tools.set(toolCallId, { name: toolName, since: Date.now() });
2348
+ }
2349
+ });
2350
+ pi.on("tool_execution_end", (e) => {
2351
+ g.tools.delete(String(e?.toolCallId));
2352
+ });
1617
2353
  pi.on("agent_end", (e) => {
1618
- g.endText = lastAssistantText(e);
2354
+ g.tools.clear();
2355
+ const cut = lastAssistantText(e);
2356
+ g.endText = cut.text;
2357
+ g.endTextTruncated = cut.truncated;
1619
2358
  g.endError = lastAssistantError(e);
1620
2359
  });
1621
2360
  function sessionFileOf(c) {
@@ -1630,9 +2369,12 @@ function serveCtl(pi) {
1630
2369
  g.ctx = c;
1631
2370
  g.state = "idle";
1632
2371
  g.since = Date.now();
2372
+ g.tools.clear();
1633
2373
  g.runs++;
1634
2374
  g.lastText = g.endText ?? "";
2375
+ g.lastTextTruncated = g.endTextTruncated === true;
1635
2376
  g.endText = void 0;
2377
+ g.endTextTruncated = void 0;
1636
2378
  g.lastError = g.endError;
1637
2379
  g.endError = void 0;
1638
2380
  g.sessionFile = sessionFileOf(c).sessionFile;
@@ -1693,6 +2435,8 @@ function serveCtl(pi) {
1693
2435
  });
1694
2436
  }
1695
2437
  function command(req) {
2438
+ const stale = incarnationRefusal(req);
2439
+ if (stale) return stale;
1696
2440
  const name = CTL_COMMANDS.find((n) => n === req.name);
1697
2441
  if (name === void 0) {
1698
2442
  return fail(`command must be one of ${CTL_COMMANDS.join(", ")}`);
@@ -1718,8 +2462,12 @@ function serveCtl(pi) {
1718
2462
  });
1719
2463
  }
1720
2464
  function fresh(req) {
2465
+ const stale = incarnationRefusal(req, "expectIncarnation");
2466
+ if (stale) return stale;
1721
2467
  const text = str(req, "text");
1722
2468
  if (text === void 0) return fail("fresh needs a string text");
2469
+ const id = msgIdOf(req);
2470
+ if (id.error) return fail(id.error);
1723
2471
  if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
1724
2472
  const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
1725
2473
  if (busy && req.force !== true) return fail("busy");
@@ -1730,6 +2478,7 @@ function serveCtl(pi) {
1730
2478
  armed: false,
1731
2479
  settle: (r) => {
1732
2480
  clearTimeout(timer);
2481
+ if (r.ok) remember(id.id, r.runs ?? g.runs);
1733
2482
  resolve(r);
1734
2483
  }
1735
2484
  };
@@ -1738,6 +2487,17 @@ function serveCtl(pi) {
1738
2487
  ).catch((e) => settleFresh(fail(e instanceof Error ? e.message : String(e))));
1739
2488
  });
1740
2489
  }
2490
+ function incarnationRefusal(req, key = "expectIncarnation") {
2491
+ if (!(key in req) || req[key] === g.incarnation) return void 0;
2492
+ return { ...fail(INCARNATION_MISMATCH), incarnation: g.incarnation };
2493
+ }
2494
+ function msgIdOf(req) {
2495
+ if (!("msgId" in req)) return {};
2496
+ const id = req.msgId;
2497
+ if (typeof id === "string" && id.length >= 1 && id.length <= CTL_MSGID_MAX_LENGTH)
2498
+ return { id };
2499
+ return { error: `msgId must be a string of 1-${CTL_MSGID_MAX_LENGTH} characters` };
2500
+ }
1741
2501
  async function handle(req, conn) {
1742
2502
  switch (req.op) {
1743
2503
  case "hello":
@@ -1747,18 +2507,31 @@ function serveCtl(pi) {
1747
2507
  agent: process.env.MU_AGENT_NAME,
1748
2508
  workstream: process.env.MU_WORKSTREAM,
1749
2509
  piVersion,
2510
+ incarnation: g.incarnation,
1750
2511
  ops: [...CTL_OPS],
2512
+ features: [...CTL_FEATURES],
1751
2513
  ...EXT_VERSION !== void 0 ? { extVersion: EXT_VERSION } : {}
1752
2514
  };
1753
- case "status":
1754
- return { v: V, ok: true, ...status() };
2515
+ case "status": {
2516
+ const id = msgIdOf(req);
2517
+ if (id.error) return fail(id.error);
2518
+ if (id.id === void 0) return { v: V, ok: true, ...status() };
2519
+ const got = g.accepted.get(id.id);
2520
+ const msg = got ? { id: id.id, accepted: true, ...got } : { id: id.id, accepted: false };
2521
+ return { v: V, ok: true, ...status(), msg };
2522
+ }
1755
2523
  case "send": {
2524
+ const stale = incarnationRefusal(req);
2525
+ if (stale) return stale;
1756
2526
  const text = str(req, "text");
1757
2527
  if (text === void 0) return fail("send needs a string text");
2528
+ const id = msgIdOf(req);
2529
+ if (id.error) return fail(id.error);
1758
2530
  const mode = req.mode === "steer" ? "steer" : "followUp";
1759
2531
  const before = status();
1760
2532
  if (g.ctx?.isIdle() ?? g.state === "idle") await g.pi.sendUserMessage(text);
1761
2533
  else await g.pi.sendUserMessage(text, { deliverAs: mode });
2534
+ remember(id.id, before.runs);
1762
2535
  return { v: V, ok: true, ...before };
1763
2536
  }
1764
2537
  case "fresh":
@@ -1766,6 +2539,9 @@ function serveCtl(pi) {
1766
2539
  case "command":
1767
2540
  return command(req);
1768
2541
  case "wait":
2542
+ if ("incarnation" in req && req.incarnation !== g.incarnation) {
2543
+ return { ...fail(INCARNATION_MISMATCH), incarnation: g.incarnation };
2544
+ }
1769
2545
  return wait(
1770
2546
  num(req, "afterRuns"),
1771
2547
  num(req, "timeoutMs"),
@@ -1786,8 +2562,12 @@ function serveCtl(pi) {
1786
2562
  }
1787
2563
  }
1788
2564
  async function interrupt(req, conn) {
2565
+ const stale = incarnationRefusal(req);
2566
+ if (stale) return stale;
1789
2567
  const text = str(req, "text");
1790
2568
  if (text === void 0) return fail("interrupt needs a string text");
2569
+ const id = msgIdOf(req);
2570
+ if (id.error) return fail(id.error);
1791
2571
  const wasBusy = !(g.ctx?.isIdle() ?? g.state === "idle");
1792
2572
  const { pending } = status();
1793
2573
  if (wasBusy) {
@@ -1799,10 +2579,12 @@ function serveCtl(pi) {
1799
2579
  }
1800
2580
  const before = status();
1801
2581
  await g.pi.sendUserMessage(text);
2582
+ remember(id.id, before.runs);
1802
2583
  return { v: V, ok: true, ...before, pending, wasBusy };
1803
2584
  }
1804
2585
  const settledRun = () => ({
1805
2586
  lastText: g.lastText,
2587
+ ...g.lastTextTruncated === true ? { lastTextTruncated: true } : {},
1806
2588
  ...g.lastError !== void 0 ? { lastError: g.lastError } : {},
1807
2589
  ...g.sessionFile !== void 0 ? { sessionFile: g.sessionFile } : {}
1808
2590
  });
@@ -1926,8 +2708,8 @@ function serveCtl(pi) {
1926
2708
  await bind();
1927
2709
  });
1928
2710
  pi.on("session_shutdown", async (e) => {
1929
- const reason = reasonOf(e);
1930
- if (reason === "new" || reason === "resume" || reason === "fork" || reason === "reload") {
2711
+ const reason2 = reasonOf(e);
2712
+ if (reason2 === "new" || reason2 === "resume" || reason2 === "fork" || reason2 === "reload") {
1931
2713
  return;
1932
2714
  }
1933
2715
  const s = g.server;
@@ -1957,7 +2739,6 @@ export {
1957
2739
  isUiBlocked,
1958
2740
  lastAssistantError,
1959
2741
  lastAssistantText,
1960
- promptsDir,
1961
2742
  uiBlocked
1962
2743
  };
1963
2744
  //# sourceMappingURL=mu-pi.js.map