@mu-crew/mu 3.13.3 → 4.1.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-4ULY3ZKO.js +4482 -0
  4. package/dist/chunk-4ULY3ZKO.js.map +1 -0
  5. package/dist/chunk-7M634JMD.js +375 -0
  6. package/dist/chunk-7M634JMD.js.map +1 -0
  7. package/dist/{chunk-3BARPJLY.js → chunk-PZ5GOCWH.js} +16 -2
  8. package/dist/chunk-PZ5GOCWH.js.map +1 -0
  9. package/dist/chunk-SS7NQSSZ.js +3660 -0
  10. package/dist/chunk-SS7NQSSZ.js.map +1 -0
  11. package/dist/chunk-UCZPJX7K.js +14253 -0
  12. package/dist/chunk-UCZPJX7K.js.map +1 -0
  13. package/dist/{cli-CKOMHBXV.js → cli-TV3N47AZ.js} +7317 -4593
  14. package/dist/cli-TV3N47AZ.js.map +1 -0
  15. package/dist/cli.js +2 -3
  16. package/dist/cli.js.map +1 -1
  17. package/dist/errors-GRONKWFS.js +65 -0
  18. package/dist/extension/mu-pi.js +1632 -837
  19. package/dist/extension/mu-pi.js.map +1 -1
  20. package/dist/index.d.ts +4067 -2561
  21. package/dist/index.js +614 -297
  22. package/dist/index.js.map +1 -1
  23. package/dist/tui-FUBQ326D.js +1848 -0
  24. package/dist/tui-FUBQ326D.js.map +1 -0
  25. package/docs/ARCHITECTURE.md +118 -66
  26. package/docs/VOCABULARY.md +113 -49
  27. package/docs/guide/README.md +3 -3
  28. package/docs/guide/backends.md +8 -4
  29. package/docs/guide/cleanup.md +69 -33
  30. package/docs/guide/delegate.md +149 -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 +122 -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 +24 -32
  41. package/docs/reference/naming.md +14 -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 +17 -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-GRONKWFS.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,448 @@ 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)}` };
679
+ }
680
+ class SpawnFail extends Error {
681
+ constructor(message, kind) {
682
+ super(message);
683
+ this.kind = kind;
684
+ }
685
+ kind;
387
686
  }
388
- async function spawn(params, text, cwd, signal) {
389
- signal?.throwIfAborted();
390
- pruneFailed();
391
- const stem = labelStem(params.label);
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 = o.startedAt ?? Date.now();
907
+ const left = Math.max(1, Math.ceil(o.timeoutS - (Date.now() - started) / 1e3));
908
+ const waiting = mu(
909
+ [
910
+ "agent",
911
+ "wait",
912
+ name,
913
+ "-w",
914
+ ws,
915
+ "--json",
916
+ "--timeout",
917
+ String(left),
918
+ "--after-runs",
919
+ String(runs)
920
+ ],
921
+ entry2.abort.signal
530
922
  );
923
+ const settle = async (r) => {
924
+ trace("settle", { call: call2.callId, name, code: r.code });
925
+ forget(key);
926
+ try {
927
+ await deliver(
928
+ call2,
929
+ name,
930
+ r,
931
+ o.keep,
932
+ {
933
+ timeoutS: o.timeoutS,
934
+ elapsedMs: Date.now() - started,
935
+ ...o.queuedAs ? { queuedAs: o.queuedAs } : {},
936
+ ...o.attach ? { attach: o.attach } : {},
937
+ ...o.workspace ? { workspace: o.workspace } : {},
938
+ ...entry2.sessionFile ? { sessionFile: entry2.sessionFile } : {}
939
+ },
940
+ entry2.identity
941
+ );
942
+ } catch (e) {
943
+ 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}`;
944
+ await Promise.resolve(
945
+ send(
946
+ {
947
+ customType: DELEGATE_MESSAGE_TYPE,
948
+ content,
949
+ display: true,
950
+ details: { name, workstream: ws, outcome: null, closed: false }
951
+ },
952
+ { deliverAs: "followUp", triggerTurn: true },
953
+ "deliver-failed"
954
+ )
955
+ ).catch(() => {
956
+ });
957
+ }
958
+ };
959
+ void waiting.then(async (r) => {
960
+ if (inflight.get(key) !== entry2) return;
961
+ if (entry2.cancelling) entry2.parked = () => settle(r);
962
+ else await settle(r);
963
+ });
964
+ return entry2;
531
965
  }
532
- async function start(params, signal, ctx, queuedAs, record) {
966
+ async function start(call2, signal, ctx, queuedAs) {
533
967
  let slotHeld = true;
534
968
  const freeSlot = () => {
535
969
  if (slotHeld) {
536
970
  starting--;
537
- showStatus();
971
+ reportPending();
538
972
  }
539
973
  slotHeld = false;
540
974
  };
541
- showStatus();
975
+ reportPending();
976
+ const ws = call2.workstream;
977
+ const req = call2.request;
542
978
  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))
979
+ const keep = req.keep === true;
980
+ const timeoutS = req.timeoutS ?? DELEGATE_TIMEOUT_S;
981
+ let cwd;
982
+ if (req.workspace !== true) {
983
+ cwd = req.cwd ? req.cwd : ctx?.cwd ?? process.cwd();
984
+ if (!isDir(cwd))
985
+ throw new SpawnFail(`mu_delegate: cwd ${cwd} is not a directory`, "unsent");
986
+ }
987
+ let spawned;
988
+ try {
989
+ spawned = await spawn(call2, req.text, cwd, signal);
990
+ } catch (e) {
991
+ if (e instanceof SpawnFail && e.kind === "nomux") {
992
+ freeSlot();
993
+ return startProcess(call2);
994
+ }
995
+ const fail2 = e instanceof SpawnFail ? e : new SpawnFail(errText(e), "unsent");
996
+ await settleFailedStart(call2, fail2);
548
997
  throw new Error(
549
- `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
998
+ 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
999
  );
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
1000
  }
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,
1001
+ const { name, runs, attach, workspace, sessionFile, identity } = spawned;
1002
+ await watch(call2, name, runs, {
1003
+ keep,
1004
+ timeoutS,
1005
+ ...queuedAs ? { queuedAs } : {},
1006
+ ...attach ? { attach } : {},
1007
+ ...workspace ? { workspace } : {},
568
1008
  ...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
- );
1009
+ ...identity ? { identity } : {},
1010
+ tracked: async () => {
1011
+ freeSlot();
1012
+ reportPending();
1013
+ await calls.mark(call2.callId, "running", { agent: name }).catch((e) => {
1014
+ trace("mark.running.failed", { call: call2.callId, error: errText(e) });
1015
+ });
632
1016
  }
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
1017
  });
639
1018
  const lines = [
640
- `Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
1019
+ `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
1020
  workspace ? `Workspace: ${workspace}` : void 0,
642
1021
  attach ? `Attach: ${attach}` : void 0,
643
1022
  `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
@@ -646,7 +1025,10 @@ ${task}` : task,
646
1025
  content: [{ type: "text", text: lines.join("\n") }],
647
1026
  details: {
648
1027
  name,
649
- workstream: W,
1028
+ workstream: ws,
1029
+ task: `${ws}/${call2.task}`,
1030
+ state: "running",
1031
+ call_id: call2.callId,
650
1032
  attach: attach ?? null,
651
1033
  workspace: workspace ?? null,
652
1034
  keep
@@ -657,11 +1039,92 @@ ${task}` : task,
657
1039
  drain();
658
1040
  }
659
1041
  }
1042
+ const finishOrphan = (call2, outcome, text) => calls.finish(call2.callId, { outcome, text, author: call2.caller }).catch((e) => {
1043
+ trace("finishOrphan.failed", { call: call2.callId, error: errText(e) });
1044
+ return void 0;
1045
+ });
1046
+ function startProcess(call2) {
1047
+ const ws = call2.workstream;
1048
+ const name = call2.task;
1049
+ const key = inflightKey(ws, name);
1050
+ const entry2 = { abort: new AbortController(), call: call2, cancelling: false };
1051
+ inflight.set(key, entry2);
1052
+ reportPending();
1053
+ const settle = async (r) => {
1054
+ const o = json(r) ?? {};
1055
+ forget(key);
1056
+ if (o.outcome === void 0 || o.state === "refused") {
1057
+ const text = processFailedText(call2, o, why(r), r.code, DELEGATE_REISSUE_TOOL);
1058
+ if (o.state !== "refused") await finishOrphan(call2, "error", text);
1059
+ startFailed(name, new Error(text), "Delegate");
1060
+ return;
1061
+ }
1062
+ const verdict = await calls.show(call2.callId).then((x) => x.verdict ?? void 0).catch(() => void 0);
1063
+ owed.set(call2.callId, {
1064
+ callId: call2.callId,
1065
+ name,
1066
+ workstream: ws,
1067
+ task: call2.task,
1068
+ outcome: o.outcome,
1069
+ ...verdict ? { verdict } : {},
1070
+ ...call2.record && o.state === "done" ? { record: `${call2.record.workstream}/${call2.record.task}` } : {},
1071
+ at: Date.now()
1072
+ });
1073
+ wakeIfIdle();
1074
+ };
1075
+ const running = mu(
1076
+ ["delegates", "run", call2.callId, "--no-keep", "--json"],
1077
+ entry2.abort.signal,
1078
+ void 0,
1079
+ { drainMs: 2e4 }
1080
+ );
1081
+ entry2.process = running;
1082
+ void running.then(async (r) => {
1083
+ if (inflight.get(key) !== entry2) return;
1084
+ await settle(r);
1085
+ });
1086
+ return processStarted(call2, DELEGATE_CANCEL_TOOL);
1087
+ }
1088
+ async function settleFailedStart(call2, fail2) {
1089
+ try {
1090
+ if (fail2.kind === "unsent") await calls.mark(call2.callId, "refused", { error: fail2.message });
1091
+ else
1092
+ await calls.finish(call2.callId, {
1093
+ outcome: fail2.kind === "aborted" ? "cancelled" : fail2.kind === "unknown" ? "unknown" : "error",
1094
+ text: fail2.message,
1095
+ author: call2.caller
1096
+ });
1097
+ } catch (e) {
1098
+ trace("settleFailedStart.failed", { call: call2.callId, error: errText(e) });
1099
+ }
1100
+ }
1101
+ const callIdIn = (name) => name.startsWith("pi:");
1102
+ const admission = () => {
1103
+ const max = delegateMax();
1104
+ if (inflight.size + starting < max) return "starting";
1105
+ return queue.length < DELEGATE_QUEUE_FACTOR * max ? "queued" : "refused";
1106
+ };
1107
+ const queuedResult = (call2, state, handle, position) => {
1108
+ const max = delegateMax();
1109
+ const taskRef = `${call2.workstream}/${call2.task}`;
1110
+ 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}" }`;
1111
+ return {
1112
+ content: [{ type: "text", text }],
1113
+ details: {
1114
+ name: handle,
1115
+ workstream: call2.workstream,
1116
+ task: taskRef,
1117
+ state,
1118
+ call_id: call2.callId,
1119
+ ...state === "queued" ? { queued: true, position } : { refused: true }
1120
+ }
1121
+ };
1122
+ };
660
1123
  pi.registerTool({
661
1124
  name: DELEGATE_TOOL,
662
1125
  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",
1126
+ 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).`,
1127
+ promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later, with a settle",
665
1128
  parameters: {
666
1129
  type: "object",
667
1130
  properties: {
@@ -676,16 +1139,16 @@ ${task}` : task,
676
1139
  },
677
1140
  cwd: {
678
1141
  type: "string",
679
- description: "Working directory for the delegate (default: yours; ignored with workspace)"
1142
+ description: "Working directory for the delegate (default: yours); with workspace, the git repo the worktree is made from"
680
1143
  },
681
1144
  timeout: {
682
1145
  type: "number",
683
1146
  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.`
1147
+ 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
1148
  },
686
1149
  workspace: {
687
1150
  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."
1151
+ 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
1152
  },
690
1153
  cli: {
691
1154
  type: "string",
@@ -697,24 +1160,31 @@ ${task}` : task,
697
1160
  },
698
1161
  keep: {
699
1162
  type: "boolean",
700
- description: "Keep the pane after it finishes, to talk to it again"
1163
+ description: "Keep the pane after it finishes, to talk to it again (needs a mux; ignored without one)"
701
1164
  },
702
1165
  record: {
703
1166
  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>.",
1167
+ 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
1168
  properties: {
706
1169
  task: { type: "string" },
707
1170
  workstream: { type: "string" }
708
1171
  },
709
1172
  required: ["task"],
710
1173
  additionalProperties: false
1174
+ },
1175
+ wave: {
1176
+ type: "string",
1177
+ description: "Label grouping the calls of one fan-out; `mu delegates tally -w <workstream> --wave <label>` counts their verdicts per judged task."
1178
+ },
1179
+ workstream: {
1180
+ type: "string",
1181
+ description: `Workstream that holds the delegate task and runs the delegate (default: ${W}).`
711
1182
  }
712
1183
  },
713
1184
  required: ["task"],
714
1185
  additionalProperties: false
715
1186
  },
716
1187
  async execute(_id, params, signal, _onUpdate, ctx) {
717
- track(ctx);
718
1188
  const task = typeof params.task === "string" ? params.task.trim() : "";
719
1189
  if (!task) throw new Error("mu_delegate needs a non-empty task");
720
1190
  const t = params.timeout;
@@ -722,179 +1192,437 @@ ${task}` : task,
722
1192
  throw new Error(
723
1193
  `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
724
1194
  );
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`);
1195
+ const callCwd = typeof params.cwd === "string" && params.cwd ? params.cwd : params.workspace !== true ? ctx?.cwd ?? process.cwd() : void 0;
1196
+ if (callCwd !== void 0 && !isDir(callCwd))
1197
+ throw new Error(`mu_delegate: cwd ${callCwd} is not a directory`);
1198
+ let model;
1199
+ if (params.model !== void 0 && params.model !== "") {
1200
+ model = normalizeModel(params.model);
1201
+ if (!model)
1202
+ throw new Error(
1203
+ `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`
1204
+ );
1205
+ }
1206
+ let workstream = W;
1207
+ if (params.workstream !== void 0) {
1208
+ const problem = workstreamNameProblem(
1209
+ typeof params.workstream === "string" ? params.workstream : String(params.workstream),
1210
+ "workstream"
1211
+ );
1212
+ if (problem !== void 0) throw new Error(`mu_delegate: ${problem} Nothing spawned.`);
1213
+ workstream = params.workstream;
1214
+ }
1215
+ if (params.wave !== void 0 && (typeof params.wave !== "string" || params.wave.trim() === ""))
1216
+ throw new Error(
1217
+ `mu_delegate: wave must be a non-empty label (got ${JSON.stringify(params.wave)})`
1218
+ );
1219
+ const record = await resolveRecord(params.record);
1220
+ const brief = typeof params.brief === "string" ? params.brief.trim() : "";
1221
+ const label = typeof params.label === "string" && params.label.trim() ? params.label.trim() : void 0;
1222
+ const decision = admission();
1223
+ const handle = decision === "starting" ? "" : `queued-${++queueSeq}`;
1224
+ if (decision === "starting") starting++;
1225
+ const session = piSessionId(ctx) || process.env.PI_SESSION_ID || `local-${token}`;
1226
+ const caller = `pi:${session}`;
1227
+ const info = {
1228
+ callId: `${caller}:${token}-${++callSeq}`,
1229
+ caller,
1230
+ workstream,
1231
+ ...typeof params.wave === "string" ? { wave: params.wave.trim() } : {},
1232
+ ...record ? { record } : {},
1233
+ request: {
1234
+ text: brief ? `${brief}
1235
+
1236
+ ${task}` : task,
1237
+ ...label ? { label } : {},
1238
+ ...typeof params.cli === "string" && params.cli ? { cli: params.cli } : {},
1239
+ ...model ? { model } : {},
1240
+ ...callCwd !== void 0 ? { cwd: callCwd } : {},
1241
+ ...params.workspace === true ? { workspace: true } : {},
1242
+ ...typeof t === "number" ? { timeoutS: Math.ceil(t) } : {},
1243
+ ...params.keep === true ? { keep: true } : {}
1244
+ }
1245
+ };
1246
+ if (decision === "starting") {
1247
+ let reg;
1248
+ try {
1249
+ reg = await calls.register(info, "starting");
1250
+ } catch (e) {
1251
+ starting--;
1252
+ reportPending();
1253
+ drain();
1254
+ throw new Error(
1255
+ `mu_delegate: could not register the call: ${errText(e)}. Nothing spawned.`
1256
+ );
1257
+ }
1258
+ return start(reg.call, signal, ctx);
728
1259
  }
729
- if (params.model !== void 0 && params.model !== "") {
730
- const m = normalizeModel(params.model);
731
- if (!m)
1260
+ if (decision === "refused") {
1261
+ let reg;
1262
+ try {
1263
+ reg = await calls.register(info, "refused");
1264
+ } catch (e) {
732
1265
  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`
1266
+ `mu_delegate: could not register the call: ${errText(e)}. Nothing spawned.`
734
1267
  );
735
- params = { ...params, model: m };
1268
+ }
1269
+ return queuedResult(reg.call, "refused", handle, 0);
736
1270
  }
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);
1271
+ const entry2 = {
1272
+ handle,
1273
+ call: { ...info, task: "" },
1274
+ ctx,
1275
+ registered: Promise.resolve()
1276
+ };
1277
+ entry2.registered = calls.register(info, "queued").then((reg) => {
1278
+ entry2.call = reg.call;
1279
+ });
1280
+ queue.push(entry2);
1281
+ reportPending();
1282
+ try {
1283
+ await entry2.registered;
1284
+ } catch (e) {
1285
+ const at = queue.indexOf(entry2);
1286
+ if (at >= 0) queue.splice(at, 1);
1287
+ reportPending();
1288
+ throw new Error(`mu_delegate: could not register the call: ${errText(e)}. Nothing queued.`);
742
1289
  }
743
- if (queue.length >= DELEGATE_QUEUE_FACTOR * max)
1290
+ return queuedResult(entry2.call, "queued", handle, queue.indexOf(entry2) + 1 || queue.length);
1291
+ }
1292
+ });
1293
+ pi.registerTool({
1294
+ name: DELEGATE_REISSUE_TOOL,
1295
+ label: "mu delegate reissue",
1296
+ 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.`,
1297
+ parameters: {
1298
+ type: "object",
1299
+ properties: { call_id: { type: "string", description: "The refused call's id" } },
1300
+ required: ["call_id"],
1301
+ additionalProperties: false
1302
+ },
1303
+ async execute(_id, params, signal, _onUpdate, ctx) {
1304
+ const callId = typeof params.call_id === "string" ? params.call_id : "";
1305
+ if (!callId) throw new Error(`${DELEGATE_REISSUE_TOOL} needs a call_id`);
1306
+ const shown = await calls.show(callId).catch((e) => {
1307
+ throw new Error(`${DELEGATE_REISSUE_TOOL}: ${errText(e)}`);
1308
+ });
1309
+ if (shown.state !== "refused")
1310
+ throw new Error(`${DELEGATE_REISSUE_TOOL}: call ${callId} is ${shown.state}, not refused.`);
1311
+ const decision = admission();
1312
+ if (decision === "refused")
744
1313
  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).`
1314
+ `${DELEGATE_REISSUE_TOOL}: still full (${inflight.size + starting} running, ${queue.length} queued). Nothing changed; reissue ${callId} when an answer arrives.`
746
1315
  );
1316
+ if (decision === "starting") starting++;
1317
+ try {
1318
+ await calls.mark(callId, decision === "starting" ? "starting" : "queued");
1319
+ } catch (e) {
1320
+ if (decision === "starting") {
1321
+ starting--;
1322
+ reportPending();
1323
+ drain();
1324
+ }
1325
+ throw new Error(`${DELEGATE_REISSUE_TOOL}: ${errText(e)}`);
1326
+ }
1327
+ if (decision === "starting") return start(shown.call, signal, ctx);
747
1328
  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
- };
1329
+ queue.push({ handle, call: shown.call, ctx, registered: Promise.resolve() });
1330
+ reportPending();
1331
+ return queuedResult(shown.call, "queued", handle, queue.length);
767
1332
  }
768
1333
  });
769
1334
  pi.registerTool({
770
1335
  name: DELEGATE_CANCEL_TOOL,
771
1336
  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.`,
1337
+ 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
1338
  parameters: {
774
1339
  type: "object",
775
1340
  properties: {
776
- name: { type: "string", description: "Delegate name, e.g. delegate-1" },
1341
+ name: {
1342
+ type: "string",
1343
+ description: "Delegate name (delegate-1), queue handle (queued-2) or call_id"
1344
+ },
777
1345
  keep: { type: "boolean", description: "Leave the pane open after aborting" }
778
1346
  },
779
1347
  required: ["name"],
780
1348
  additionalProperties: false
781
1349
  },
782
- async execute(_id, params, _signal, _onUpdate, ctx) {
783
- track(ctx);
1350
+ async execute(_id, params) {
784
1351
  const name = typeof params.name === "string" ? params.name : "";
785
1352
  if (!name) throw new Error("mu_delegate_cancel needs a name");
786
- const qi = queue.findIndex((q) => q.handle === name);
1353
+ const cancelCall = async (call2, author, text2) => {
1354
+ try {
1355
+ const f = await calls.finish(call2.callId, {
1356
+ outcome: "cancelled",
1357
+ ...text2 ? { text: text2 } : {},
1358
+ author
1359
+ });
1360
+ if (f.recorded && call2.record)
1361
+ return ` Recorded on ${call2.record.workstream}/${call2.record.task} as a note (no verdict: cancelled).`;
1362
+ return f.changed ? ` Task ${call2.workstream}/${call2.task} closed (cancelled).` : "";
1363
+ } catch (e) {
1364
+ return ` Recording the cancel failed: ${errText(e)}.`;
1365
+ }
1366
+ };
1367
+ const qi = queue.findIndex((q) => q.handle === name || q.call.callId === name);
787
1368
  if (qi >= 0) {
788
1369
  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))}` : "";
1370
+ reportPending();
1371
+ let recorded = "";
1372
+ if (q) {
1373
+ await q.registered.catch(() => {
1374
+ });
1375
+ recorded = await cancelCall(q.call, q.handle);
1376
+ }
791
1377
  return {
792
1378
  content: [
793
1379
  { type: "text", text: `Dropped ${name} from the queue; it never started.${recorded}` }
794
1380
  ],
795
- details: { name, workstream: W }
1381
+ details: { name, workstream: q?.call.workstream ?? W }
1382
+ };
1383
+ }
1384
+ const found = [...inflight].find(
1385
+ ([key2, e]) => key2 === name || key2 === inflightKey(e.call.workstream, name) || e.call.callId === name
1386
+ );
1387
+ if (!found && callIdIn(name)) {
1388
+ const shown = await calls.show(name).catch((e) => {
1389
+ throw new Error(`mu_delegate_cancel: ${errText(e)}`);
1390
+ });
1391
+ const recorded = await cancelCall(shown.call, name);
1392
+ return {
1393
+ content: [{ type: "text", text: `Cancelled ${name} (${shown.state}).${recorded}` }],
1394
+ details: { name, workstream: shown.call.workstream }
1395
+ };
1396
+ }
1397
+ const key = found?.[0];
1398
+ const entry2 = found?.[1];
1399
+ const ws = entry2?.call.workstream ?? W;
1400
+ const agentName = key ? key.slice(key.indexOf("/") + 1) : name;
1401
+ if (entry2?.process && key) {
1402
+ entry2.abort.abort();
1403
+ forget(key);
1404
+ await entry2.process;
1405
+ await finishOrphan(entry2.call, "cancelled", "cancelled");
1406
+ const shown = await calls.show(entry2.call.callId).catch(() => void 0);
1407
+ const where = entry2.call.record ? ` Recorded on ${entry2.call.record.workstream}/${entry2.call.record.task} as a note (no verdict: cancelled).` : "";
1408
+ return {
1409
+ content: [
1410
+ {
1411
+ type: "text",
1412
+ text: `Cancelled ${ws}/${agentName} (child pi stopped; call ${shown?.state ?? "cancelling"}).${where}`
1413
+ }
1414
+ ],
1415
+ details: { name: agentName, workstream: ws }
796
1416
  };
797
1417
  }
798
- const entry = inflight.get(name);
799
- if (entry) entry.cancelling = true;
800
- const ab = await mu(["agent", "abort", name, "-w", W]);
1418
+ if (entry2) entry2.cancelling = true;
1419
+ const ab = await mu(["agent", "abort", agentName, "-w", ws]);
801
1420
  if (ab.code !== 0) {
802
- if (entry) {
803
- entry.cancelling = false;
804
- const parked = entry.parked;
805
- entry.parked = void 0;
1421
+ if (entry2) {
1422
+ entry2.cancelling = false;
1423
+ const parked = entry2.parked;
1424
+ entry2.parked = void 0;
806
1425
  if (parked) void parked();
807
1426
  }
808
- throw failure(`mu agent abort ${W}/${name}`, ab);
1427
+ throw failure(`mu agent abort ${ws}/${agentName}`, ab);
809
1428
  }
810
- if (entry) {
811
- entry.abort.abort();
812
- forget(name);
1429
+ if (entry2 && key) {
1430
+ entry2.abort.abort();
1431
+ forget(key);
813
1432
  }
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))}`;
1433
+ let text = `Aborted ${ws}/${agentName}.`;
1434
+ const sessionFile = entry2?.sessionFile;
1435
+ const evidence = evidenceLines({ ...sessionFile ? { sessionFile } : {} });
1436
+ if (entry2)
1437
+ text += await cancelCall(
1438
+ entry2.call,
1439
+ agentName,
1440
+ sessionFile ? lastAssistantTextIn(sessionFile) : void 0
1441
+ );
824
1442
  if (params.keep === true) text += " Pane kept.";
825
1443
  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()}`;
1444
+ const c = await closeDelegate(agentName, entry2?.identity, ws);
1445
+ text += c.closed ? " Pane closed." : ` Pane kept: ${c.why}`;
828
1446
  }
829
1447
  if (evidence.length > 0) text += `
830
1448
  ${evidence.join("\n")}`;
831
- return { content: [{ type: "text", text }], details: { name, workstream: W } };
1449
+ return { content: [{ type: "text", text }], details: { name: agentName, workstream: ws } };
832
1450
  }
833
1451
  });
834
1452
  pi.on("message_start", (e) => {
835
1453
  const m = e.message;
836
1454
  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();
1455
+ const id = m.details?.wake;
1456
+ if (typeof id !== "number") return;
1457
+ for (const c of arriving.get(id)?.calls ?? []) owed.delete(c);
1458
+ arriving.delete(id);
839
1459
  });
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;
1460
+ const dropWakes = (ids, also = []) => {
1461
+ const names = new Set(also);
1462
+ for (const id of ids) {
1463
+ const a = arriving.get(id);
1464
+ if (!a) continue;
845
1465
  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
- });
1466
+ for (const o of a.opts ?? []) o.triggerTurn = false;
1467
+ for (const c of a.calls) names.add(c);
853
1468
  }
1469
+ const dropped = [];
1470
+ for (const c of names) {
1471
+ const a = owed.get(c);
1472
+ if (!a) continue;
1473
+ owed.delete(c);
1474
+ dropped.push(a);
1475
+ }
1476
+ trace("dropped", { calls: dropped.map((d) => d.callId) });
854
1477
  if (dropped.length === 0) return;
855
- showStatus();
856
1478
  void Promise.resolve(
857
- pi.sendMessage(
1479
+ send(
858
1480
  {
859
1481
  customType: DELEGATE_MESSAGE_TYPE,
860
1482
  content: droppedNote(dropped),
861
1483
  display: true,
862
- details: { dropped }
1484
+ details: { dropped: dropped.map((d) => d.callId) }
863
1485
  },
864
- { triggerTurn: false }
1486
+ { triggerTurn: false },
1487
+ "dropped-note"
865
1488
  )
866
1489
  ).catch(() => {
867
1490
  });
1491
+ };
1492
+ const recoverLost = (candidates, ctx) => {
1493
+ settleCheck = void 0;
1494
+ if (closed) return;
1495
+ let idle = false;
1496
+ try {
1497
+ idle = ctx.isIdle?.() ?? true;
1498
+ } catch {
1499
+ }
1500
+ if (!idle) return;
1501
+ if ([...arriving].some(([id, a2]) => a2.sent && !candidates.includes(id))) return;
1502
+ const lost = candidates.filter((id) => arriving.get(id)?.sent);
1503
+ dropWakes(lost.filter((id) => arriving.get(id)?.resent));
1504
+ const next = lost.find((id) => arriving.has(id));
1505
+ const a = next === void 0 ? void 0 : arriving.get(next);
1506
+ if (next === void 0 || !a?.message) {
1507
+ wake("wake-after-settle");
1508
+ return;
1509
+ }
1510
+ a.resent = true;
1511
+ trace("resend", { wake: next, calls: a.calls });
1512
+ void Promise.resolve(
1513
+ send(a.message, { deliverAs: "followUp", triggerTurn: true }, "resend")
1514
+ ).catch(() => {
1515
+ arriving.delete(next);
1516
+ });
1517
+ };
1518
+ pi.on("agent_start", () => {
1519
+ busy = true;
868
1520
  });
869
- pi.on("session_shutdown", (_e, ctx) => {
870
- if (arriving.size > 0) {
871
- arriving.clear();
872
- showStatus();
1521
+ pi.on("agent_settled", (e, ctx) => {
1522
+ busy = false;
1523
+ const sent = [...arriving].filter(([, a]) => a.sent).map(([id]) => id);
1524
+ if (e.aborted === true) {
1525
+ clearImmediate(settleCheck);
1526
+ dropWakes(sent, [...owed.keys()]);
1527
+ return;
1528
+ }
1529
+ if (sent.length === 0 && owed.size === 0) return;
1530
+ clearImmediate(settleCheck);
1531
+ settleCheck = setImmediate(() => recoverLost(sent, ctx));
1532
+ });
1533
+ pi.on("session_start", async (_e, ctx) => {
1534
+ const session = piSessionId(ctx) || process.env.PI_SESSION_ID;
1535
+ if (!session || closed) return;
1536
+ const r = await mu(["delegates", "list", "--caller", `pi:${session}`, "--json"]).catch(
1537
+ () => void 0
1538
+ );
1539
+ const items = (r ? json(r)?.items : void 0) ?? [];
1540
+ for (const row of items) {
1541
+ if (row.state !== "running" || typeof row.agent !== "string" || row.agent === row.task)
1542
+ continue;
1543
+ const call2 = callInfoOf(row);
1544
+ if (call2.caller !== `pi:${session}` || inflight.has(inflightKey(call2.workstream, row.agent)))
1545
+ continue;
1546
+ const since = typeof row.updatedAt === "string" ? Date.parse(row.updatedAt) : Number.NaN;
1547
+ await watch(call2, row.agent, 0, {
1548
+ keep: call2.request.keep === true,
1549
+ timeoutS: call2.request.timeoutS ?? DELEGATE_TIMEOUT_S,
1550
+ ...Number.isFinite(since) ? { startedAt: since } : {}
1551
+ });
873
1552
  }
1553
+ });
1554
+ pi.on("session_shutdown", async (e, ctx) => {
1555
+ closed = true;
1556
+ clearImmediate(settleCheck);
1557
+ clearImmediate(wakeSoon);
1558
+ trace("session_shutdown.state", {
1559
+ arriving: [...arriving.values()].map((a) => ({ calls: a.calls, sent: a.sent })),
1560
+ owed: [...owed.keys()],
1561
+ inflight: [...inflight.keys()]
1562
+ });
1563
+ arriving.clear();
1564
+ owed.clear();
874
1565
  if (inflight.size === 0 && queue.length === 0) return;
875
- const names = [...inflight.keys()];
1566
+ const shown = (key) => key.startsWith(`${W}/`) ? key.slice(W.length + 1) : key;
1567
+ const names = [...inflight].flatMap(([k, e2]) => e2.process ? [] : [shown(k)]);
1568
+ const procs = [...inflight].flatMap(([k, e2]) => e2.process ? [shown(k)] : []);
876
1569
  const sessions = [...inflight].flatMap(
877
- ([n, e]) => e.sessionFile ? [`${n}: ${e.sessionFile}`] : []
1570
+ ([k, e2]) => e2.sessionFile ? [`${shown(k)}: ${e2.sessionFile}`] : []
878
1571
  );
879
- const never = queue.splice(0).map((q) => q.handle);
880
- for (const e of inflight.values()) e.abort.abort();
1572
+ const dropped = queue.splice(0);
1573
+ const reason2 = e?.reason;
1574
+ const replacing = reason2 === "reload" || reason2 === "new" || reason2 === "resume" || reason2 === "fork";
1575
+ const stopping = [];
1576
+ for (const f of inflight.values())
1577
+ if (!f.process || !replacing) {
1578
+ f.abort.abort();
1579
+ const call2 = f.call;
1580
+ if (f.process)
1581
+ stopping.push(f.process.then(() => finishOrphan(call2, "cancelled", "pi quit")));
1582
+ }
881
1583
  inflight.clear();
882
- showStatus();
883
- ui = void 0;
1584
+ reportPending();
1585
+ await Promise.all(stopping);
1586
+ const refused = [];
1587
+ await Promise.all(
1588
+ dropped.map(async (q) => {
1589
+ try {
1590
+ await q.registered;
1591
+ await calls.mark(q.call.callId, "refused", {
1592
+ error: "its session ended before it started"
1593
+ });
1594
+ refused.push(q.call.callId);
1595
+ } catch {
1596
+ }
1597
+ })
1598
+ );
884
1599
  const parts = [];
885
1600
  if (names.length > 0)
886
1601
  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.`
1602
+ `${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.`}`
1603
+ );
1604
+ if (procs.length > 0)
1605
+ parts.push(
1606
+ 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
1607
  );
889
1608
  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.`);
1609
+ if (dropped.length > 0)
1610
+ parts.push(
1611
+ `${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)` : ""}.`
1612
+ );
892
1613
  const msg = parts.join(" ");
893
1614
  if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
894
1615
  else process.stderr.write(`mu: ${msg}
895
1616
  `);
896
1617
  });
897
- return { answerPending: () => arriving.size > 0 };
1618
+ return {
1619
+ answerPending: () => arriving.size > 0,
1620
+ delivered(shown) {
1621
+ if (Array.isArray(shown)) for (const c of shown) owed.delete(c);
1622
+ else
1623
+ for (const [c, a] of owed) if (a.at < shown.before) owed.delete(c);
1624
+ }
1625
+ };
898
1626
  }
899
1627
 
900
1628
  // extension/nudge.ts
@@ -903,14 +1631,12 @@ import { join as join3 } from "path";
903
1631
  var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
904
1632
  var NUDGE_LOG_KIND = "nudge";
905
1633
  var CLOSE_NUDGE_MESSAGE_TYPE = "mu-close-task";
906
- var REFUTE_NUDGE_MESSAGE_TYPE = "mu-refute-brief";
907
1634
  var MARK_START = "<!-- mu:keep-driving -->";
908
1635
  var MARK_END = "<!-- /mu:keep-driving -->";
909
- var WAIT_HINT_MAX = 8;
910
1636
  var notNested = () => false;
911
1637
  var noHold = () => void 0;
912
1638
  function withQueuedInput(hold) {
913
- return (ctx) => ctx?.hasPendingMessages?.() ? "answer" : hold(ctx);
1639
+ return (ctx) => ctx?.hasPendingMessages?.() ? "input" : hold(ctx);
914
1640
  }
915
1641
  function noUI(ctx) {
916
1642
  return ctx?.hasUI === false;
@@ -922,7 +1648,7 @@ function keepDrivingRule(skillMd) {
922
1648
  const i = skillMd.indexOf(MARK_START);
923
1649
  const j = skillMd.indexOf(MARK_END);
924
1650
  if (i < 0 || j < i) return void 0;
925
- const text = skillMd.slice(i + MARK_START.length, j).trim();
1651
+ const text = skillMd.slice(i + MARK_START.length, j).trim().split(/\n\s*\n/).map((p) => p.replace(/\s*\n\s*/g, " ")).join("\n\n");
926
1652
  return text === "" ? void 0 : text;
927
1653
  }
928
1654
  function readSkill() {
@@ -932,190 +1658,13 @@ function readSkill() {
932
1658
  }
933
1659
  return void 0;
934
1660
  }
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");
1661
+ async function allItems(run, argv, uncapped) {
1662
+ const r = await run([...argv, uncapped]);
1663
+ if (r.code !== 0) return void 0;
1664
+ const data = parseEnvelope(r.stdout);
1665
+ if (data === void 0 || !Array.isArray(data.items)) return void 0;
1666
+ if ((data.more ?? 0) > 0) return void 0;
1667
+ return data.items;
1119
1668
  }
1120
1669
  function breadcrumb(run, args) {
1121
1670
  void run(args).catch(() => {
@@ -1149,7 +1698,8 @@ function parallelSettle(pi, hold = noHold) {
1149
1698
  const late = hold(ctx);
1150
1699
  const fired = results.flatMap((r) => {
1151
1700
  if (r === void 0) return [];
1152
- if (late === "answer" || late === "blocked" && r.continue === true) {
1701
+ if (r.receipt === true && late !== void 0) return [{ entries: r.entries ?? [] }];
1702
+ if (late === "input" || late === "wake" || late === "blocked" && r.continue === true) {
1153
1703
  r.yielded?.();
1154
1704
  return [];
1155
1705
  }
@@ -1176,54 +1726,305 @@ function parallelSettle(pi, hold = noHold) {
1176
1726
  function outcomeOf(event) {
1177
1727
  return typeof event === "object" && event !== null ? event.outcome : void 0;
1178
1728
  }
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;
1729
+ var SETTLE_DRAIN_MS = 3e3;
1730
+ var SETTLE_WAIT_ARGS = ["wait", "--first", "--on-stall", "exit", "--json"];
1731
+ var SETTLE_READY_ARGS = [
1732
+ "wait",
1733
+ "--first",
1734
+ "--on-stall",
1735
+ "exit",
1736
+ "--timeout",
1737
+ "1",
1738
+ "--json"
1739
+ ];
1740
+ var READY_MAX = 50;
1741
+ var ESC = /^\u001b(\[27(;\d+)*u)?$/;
1742
+ function envelopeOf(stdout) {
1743
+ try {
1744
+ const v = JSON.parse(stdout);
1745
+ return typeof v === "object" && v !== null && typeof v.ok === "boolean" ? v : void 0;
1746
+ } catch {
1747
+ return void 0;
1748
+ }
1749
+ }
1750
+ function sessionIdOf(ctx) {
1751
+ try {
1752
+ return ctx?.sessionManager?.getSessionId?.() || void 0;
1753
+ } catch {
1754
+ return void 0;
1755
+ }
1756
+ }
1757
+ function identityEnvs(sid) {
1758
+ const e = process.env;
1759
+ const out = [];
1760
+ if (e.MU_MANAGED_AGENT === "1" && e.MU_WORKSTREAM && e.MU_AGENT_NAME)
1761
+ out.push({ PI_SESSION_ID: void 0 });
1762
+ if (sid !== void 0) out.push({ MU_MANAGED_AGENT: void 0, PI_SESSION_ID: sid });
1763
+ return out;
1764
+ }
1765
+ async function humanNeeds(run, sid) {
1766
+ const counts = await Promise.all(
1767
+ identityEnvs(sid).map(async (env) => {
1768
+ const r = await run(["state", "--me", "--all", "--full", "--json"], void 0, void 0, {
1769
+ env
1770
+ }).catch(() => void 0);
1771
+ const env0 = r === void 0 ? void 0 : envelopeOf(r.stdout);
1772
+ const ws = env0?.ok === true ? env0.data?.workstreams : void 0;
1773
+ if (!Array.isArray(ws)) return void 0;
1774
+ return ws.reduce(
1775
+ (n, w) => n + (w.human_needs?.length ?? 0),
1776
+ 0
1777
+ );
1778
+ })
1779
+ );
1780
+ return counts.some((c) => c === void 0) ? void 0 : counts.reduce((a, b) => a + (b ?? 0), 0);
1781
+ }
1782
+ function entry(content) {
1783
+ return { type: "custom_message", customType: NUDGE_MESSAGE_TYPE, content, display: true };
1784
+ }
1785
+ var REPORT_CAP = 4e3;
1786
+ var WAVE_REPORT_CAP = 1500;
1787
+ var PENDING_STATES = /* @__PURE__ */ new Set(["queued", "starting", "running", "recording"]);
1788
+ var recordOf = (c) => c.record?.workstream && c.record.task ? `${c.record.workstream}/${c.record.task}` : void 0;
1789
+ async function reportOf(mu, c) {
1790
+ const v = await mu(["task", "notes", c.task, "-w", c.workstream, "--json"]);
1791
+ const items = v?.ok === true ? v.data?.items : void 0;
1792
+ return items?.filter((n) => n.kind === "report").at(-1)?.content;
1793
+ }
1794
+ async function answerBlock(mu, c, cap) {
1795
+ const where = recordOf(c);
1796
+ const notes = `mu task notes ${c.task} -w ${c.workstream}`;
1797
+ const head = [
1798
+ `Delegate answer: call ${c.callId}, task ${c.workstream}/${c.task}, ${c.state}`,
1799
+ c.verdict ? `, ${c.verdict}` : "",
1800
+ where ? `, recorded on ${where}` : "",
1801
+ c.error ? `. Error: ${c.error}` : "",
1802
+ "."
1803
+ ].join("");
1804
+ if (where && c.verdict) return `${head} Full answer: ${notes}`;
1805
+ const text = await reportOf(mu, c);
1806
+ if (text === void 0) return `${head} Report unreadable here; read it: ${notes}`;
1807
+ const body = text.length > cap ? `${text.slice(0, cap)}
1808
+ [... ${text.length - cap} more characters: ${notes}]` : text;
1809
+ return `${head}
1810
+ ${body}`;
1811
+ }
1812
+ function waveSummary(wave, rows) {
1813
+ const n = (p) => rows.filter(p).length;
1814
+ const refused = rows.filter((c) => c.state === "refused");
1815
+ const counts = [
1816
+ `${n((c) => c.verdict === "CONFIRMED")}/${rows.length} CONFIRMED`,
1817
+ `${n((c) => c.verdict === "REFUTED")} REFUTED`,
1818
+ `${n((c) => c.state !== "refused" && !PENDING_STATES.has(c.state) && c.verdict !== "CONFIRMED" && c.verdict !== "REFUTED")} UNVERIFIED`,
1819
+ `${n((c) => PENDING_STATES.has(c.state))} pending`,
1820
+ `${refused.length} refused`
1821
+ ].join(", ");
1822
+ const lines = rows.map(
1823
+ (c) => `- ${recordOf(c) ?? `${c.workstream}/${c.task}`}: ${c.state === "refused" ? "REFUSED, reissue it: mu_delegate_reissue" : c.verdict ?? c.state} (call ${c.callId})`
1824
+ );
1825
+ return [`Wave ${wave}: ${counts}.`, ...lines].join("\n");
1826
+ }
1827
+ function noticesOf(d) {
1828
+ return Array.isArray(d.notices) ? d.notices.filter((n) => typeof n === "string") : [];
1829
+ }
1830
+ function firingDelegate(d) {
1831
+ const f = d.firing;
1832
+ if (f?.kind !== "delegate" || typeof f.ref !== "string") return void 0;
1833
+ return f.ref.startsWith("delegate:") ? f.ref.slice("delegate:".length) : void 0;
1834
+ }
1835
+ function pictureOf(d) {
1836
+ if (typeof d.snapshotsCut === "number" && d.snapshotsCut > 0) return void 0;
1837
+ const snaps = Array.isArray(d.snapshots) ? d.snapshots : [];
1838
+ return new Set(
1839
+ snaps.flatMap(
1840
+ (s) => typeof s.ref === "string" && s.ref.startsWith("delegate:") ? [s.ref.slice("delegate:".length)] : []
1841
+ )
1842
+ );
1183
1843
  }
1184
- function registerNudge(pi, run = defaultRunner(), skillMd = readSkill(), nested = notNested, hold = noHold) {
1844
+ var noDelivered = () => {
1845
+ };
1846
+ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill(), nested = notNested, hold = noHold, delivered = noDelivered) {
1185
1847
  if (!nudgeEnabled()) return;
1848
+ if (process.env.MU_REMOTE === "1") return;
1186
1849
  const rule = skillMd === void 0 ? void 0 : keepDrivingRule(skillMd);
1187
1850
  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
- });
1851
+ let current;
1852
+ const cancel = () => current?.abort();
1853
+ pi.on("input", cancel);
1854
+ pi.on("ui_prompt_start", cancel);
1855
+ pi.on("session_shutdown", cancel);
1199
1856
  pi.on("agent_before_settle", async (event, ctx) => {
1200
- if (fired || armed.size === 0 || nested() || hold(ctx)) return;
1857
+ if (nested() || hold(ctx)) return;
1201
1858
  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;
1859
+ const sid = sessionIdOf(ctx);
1860
+ if (identityEnvs(sid).length === 0) return;
1861
+ const env = { PI_SESSION_ID: sid };
1862
+ const ctl = new AbortController();
1863
+ current = ctl;
1864
+ const off = ctx?.ui?.onTerminalInput?.((data) => {
1865
+ if (ESC.test(data)) ctl.abort();
1866
+ return void 0;
1867
+ }) ?? (() => {
1868
+ });
1869
+ const wait = (args) => run(args, ctl.signal, void 0, { env, drainMs: SETTLE_DRAIN_MS });
1870
+ const mu = async (args) => envelopeOf(
1871
+ (await run(args, void 0, void 0, { env }).catch(() => void 0))?.stdout ?? ""
1872
+ );
1873
+ const call2 = async (id) => {
1874
+ const v = await mu(["delegates", "show", id, "--json"]);
1875
+ return v?.ok === true ? v.data?.call : void 0;
1876
+ };
1877
+ const answers = async (callId, shown, first) => {
1878
+ shown.add(callId);
1879
+ const c = await call2(callId);
1880
+ if (c === void 0)
1881
+ return {
1882
+ blocks: [
1883
+ `Delegate answer: call ${callId}. Its row is unreadable here: mu delegates show ${callId}`
1884
+ ],
1885
+ stop: false
1886
+ };
1887
+ const wave = c.wave;
1888
+ if (!wave) return { blocks: [await answerBlock(mu, c, REPORT_CAP)], stop: false };
1889
+ const listed = async () => {
1890
+ const v = await mu(["delegates", "list", "--wave", wave, "--json"]);
1891
+ const items = v?.ok === true ? v.data?.items : void 0;
1892
+ return Array.isArray(items) ? items : [c];
1893
+ };
1894
+ const after = [];
1895
+ const notices = new Set(noticesOf(first));
1896
+ let stop = false;
1897
+ let rows = await listed();
1898
+ let picture = pictureOf(first);
1899
+ const owedRow = (r) => PENDING_STATES.has(r.state) || r.state !== "refused" && !shown.has(r.callId) && picture?.has(r.callId) !== false;
1900
+ while (!ctl.signal.aborted && rows.some(owedRow)) {
1901
+ const r = await wait(SETTLE_WAIT_ARGS);
1902
+ const v = envelopeOf(r.stdout);
1903
+ if (v?.ok === false && v.error === "WaitNothingError") break;
1904
+ const d = v?.ok === true ? v.data : void 0;
1905
+ if (d === void 0) {
1906
+ after.push(
1907
+ `The wave's wait failed (exit ${r.code}): ${(r.stdout.trim() || r.stderr.trim()).slice(0, 500)}`
1908
+ );
1909
+ break;
1910
+ }
1911
+ if (d.interrupted === true || d.drained === true) break;
1912
+ for (const n of noticesOf(d)) notices.add(n);
1913
+ picture = pictureOf(d);
1914
+ if (d.timedOut === true) continue;
1915
+ if (Array.isArray(d.humanNeeds) && d.humanNeeds.length > 0) stop = true;
1916
+ const id = firingDelegate(d);
1917
+ const sibling = id !== void 0 && rows.some((x) => x.callId === id);
1918
+ if (id !== void 0) shown.add(id);
1919
+ if (sibling && !stop) {
1920
+ rows = await listed();
1921
+ continue;
1922
+ }
1923
+ after.push(`Another event came before the rest of wave ${wave}:
1924
+ ${JSON.stringify(d)}`);
1925
+ const oc = id === void 0 || sibling ? void 0 : await call2(id);
1926
+ if (oc) after.push(await answerBlock(mu, oc, REPORT_CAP));
1927
+ break;
1928
+ }
1929
+ rows = await listed();
1930
+ const complete = !ctl.signal.aborted && after.length === 0 && !rows.some(owedRow);
1931
+ const bodies = await Promise.all(
1932
+ rows.filter((x) => shown.has(x.callId)).map((x) => answerBlock(mu, x, WAVE_REPORT_CAP))
1933
+ );
1934
+ return {
1935
+ blocks: [waveSummary(wave, rows), ...bodies, ...after],
1936
+ stop,
1937
+ wave: { label: wave, complete, notices: [...notices] }
1938
+ };
1939
+ };
1940
+ const ready = async () => {
1941
+ const blocks = [];
1942
+ const shown = [];
1943
+ for (let i = 0; i < READY_MAX && !ctl.signal.aborted; i++) {
1944
+ const started = Date.now();
1945
+ const r = await wait(SETTLE_READY_ARGS);
1946
+ const v = envelopeOf(r.stdout);
1947
+ if (v?.ok === false && v.error === "WaitNothingError") {
1948
+ delivered({ before: started });
1949
+ break;
1950
+ }
1951
+ const d = v?.ok === true ? v.data : void 0;
1952
+ if (d === void 0) {
1953
+ const why = (r.stdout.trim() || r.stderr.trim()).slice(0, 2e3);
1954
+ blocks.push(`mu wait failed (exit ${r.code}): ${why}`);
1955
+ break;
1956
+ }
1957
+ if (d.drained === true) delivered({ before: started });
1958
+ if (d.interrupted === true || d.drained === true || d.timedOut === true) break;
1959
+ blocks.push(JSON.stringify(d));
1960
+ const id = firingDelegate(d);
1961
+ if (id === void 0) continue;
1962
+ shown.push(id);
1963
+ const c = await call2(id);
1964
+ blocks.push(
1965
+ c === void 0 ? `Delegate answer: call ${id}. Its row is unreadable here: mu delegates show ${id}` : await answerBlock(mu, c, REPORT_CAP)
1966
+ );
1225
1967
  }
1968
+ if (shown.length > 0) delivered(shown);
1969
+ if (blocks.length === 0) return void 0;
1970
+ const text = [
1971
+ "[mu] A human stop is open (mu human-needed), so the settle did not wait. It showed what was already ready:",
1972
+ ...blocks.flatMap((b) => ["", b])
1973
+ ].join("\n");
1974
+ return { entries: [entry(text)], receipt: true };
1226
1975
  };
1976
+ let result;
1977
+ try {
1978
+ result = await (async () => {
1979
+ for (; ; ) {
1980
+ const needs = await humanNeeds(run, sid);
1981
+ if (ctl.signal.aborted || hold(ctx)) return void 0;
1982
+ if (needs === void 0)
1983
+ return {
1984
+ entries: [
1985
+ entry(
1986
+ "[mu] settle: could not read your open human needs (mu state --me); not waiting."
1987
+ )
1988
+ ]
1989
+ };
1990
+ if (needs > 0) return ready();
1991
+ const started = Date.now();
1992
+ const r = await wait(SETTLE_WAIT_ARGS);
1993
+ const v = envelopeOf(r.stdout);
1994
+ if (v?.ok === false && v.error === "WaitNothingError") {
1995
+ delivered({ before: started });
1996
+ return void 0;
1997
+ }
1998
+ if (v === void 0 || v.ok !== true || v.data === void 0) {
1999
+ const why = (r.stdout.trim() || r.stderr.trim()).slice(0, 2e3);
2000
+ return { entries: [entry(`[mu] settle: mu wait failed (exit ${r.code}): ${why}`)] };
2001
+ }
2002
+ const d = v.data;
2003
+ if (d.drained === true) delivered({ before: started });
2004
+ if (d.interrupted === true || d.drained === true) return void 0;
2005
+ if (d.timedOut === true) continue;
2006
+ const callId = firingDelegate(d);
2007
+ const shown = /* @__PURE__ */ new Set();
2008
+ const got = callId === void 0 ? void 0 : await answers(callId, shown, d);
2009
+ const blocks = got?.blocks ?? [];
2010
+ if (shown.size > 0) delivered([...shown]);
2011
+ const humanStop = Array.isArray(d.humanNeeds) && d.humanNeeds.length > 0;
2012
+ const stop = humanStop || got?.stop === true;
2013
+ const ran = `Work you own is still running, so the settle ran \`mu ${SETTLE_WAIT_ARGS.join(" ")}\` for you.`;
2014
+ const w = got?.wave;
2015
+ const head = w !== void 0 && !humanStop ? [
2016
+ w.complete ? `${ran} It waited until wave ${w.label} was in:` : `${ran} It waited for wave ${w.label}, which is not all in (below):`,
2017
+ ...w.notices.map((n) => `Notice: ${n}`)
2018
+ ] : [`${ran} Its result:`, JSON.stringify(d)];
2019
+ const text = [`[mu] ${rule}`, "", ...head, ...blocks.flatMap((b) => ["", b])].join("\n");
2020
+ return { entries: [entry(text)], ...stop ? {} : { continue: true }, receipt: true };
2021
+ }
2022
+ })();
2023
+ } finally {
2024
+ off();
2025
+ if (current === ctl) current = void 0;
2026
+ }
2027
+ return result;
1227
2028
  });
1228
2029
  }
1229
2030
  function workerIdentity(env = process.env) {
@@ -1233,12 +2034,15 @@ function workerIdentity(env = process.env) {
1233
2034
  return { agent, workstream };
1234
2035
  }
1235
2036
  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
2037
  try {
1239
- const json = JSON.parse(r.stdout);
1240
- return (json.items ?? []).flatMap(
1241
- (t) => t.status === "IN_PROGRESS" && t.name ? [t.name] : []
2038
+ const items = await allItems(
2039
+ run,
2040
+ ["task", "owned-by", agent, "-w", workstream, "--json"],
2041
+ "--full"
2042
+ );
2043
+ if (items === void 0) return void 0;
2044
+ return items.flatMap(
2045
+ (t) => t.status === "IN_PROGRESS" && t.kind !== "delegate" && t.id ? [t.id] : []
1242
2046
  );
1243
2047
  } catch {
1244
2048
  return void 0;
@@ -1255,6 +2059,7 @@ function closeNudgeText(workstream, names) {
1255
2059
  }
1256
2060
  function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested = notNested, hold = noHold) {
1257
2061
  if (!nudgeEnabled(env)) return;
2062
+ if (env.MU_REMOTE === "1") return;
1258
2063
  const me = workerIdentity(env);
1259
2064
  if (me === void 0) return;
1260
2065
  let fired = false;
@@ -1295,74 +2100,9 @@ function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested
1295
2100
  };
1296
2101
  });
1297
2102
  }
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
2103
 
1364
2104
  // extension/mu-pi.ts
1365
- var EXT_VERSION = true ? "3.13.3" : void 0;
2105
+ var EXT_VERSION = true ? "4.1.0" : void 0;
1366
2106
  var FRESH_COMMAND = "mu-fresh";
1367
2107
  var FRESH_TIMEOUT_MS = 3e4;
1368
2108
  function commandName(name) {
@@ -1378,9 +2118,9 @@ function fail(error) {
1378
2118
  }
1379
2119
  function detectPiVersion() {
1380
2120
  try {
1381
- const entry = process.argv[1];
1382
- if (!entry) return void 0;
1383
- let dir = dirname(realpathSync(entry));
2121
+ const entry2 = process.argv[1];
2122
+ if (!entry2) return void 0;
2123
+ let dir = dirname(realpathSync(entry2));
1384
2124
  for (let i = 0; i < 6; i++) {
1385
2125
  const pkgPath = join4(dir, "package.json");
1386
2126
  if (existsSync3(pkgPath)) {
@@ -1416,22 +2156,22 @@ function sharedMap() {
1416
2156
  }
1417
2157
  function lastAssistantText(event) {
1418
2158
  const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
1419
- if (!Array.isArray(msgs)) return "";
2159
+ if (!Array.isArray(msgs)) return { text: "", truncated: false };
1420
2160
  for (let i = msgs.length - 1; i >= 0; i--) {
1421
2161
  const m = msgs[i];
1422
2162
  if (typeof m !== "object" || m === null) continue;
1423
2163
  const { role, content } = m;
1424
2164
  if (role !== "assistant") continue;
1425
- if (typeof content === "string") return capText(content);
1426
- if (!Array.isArray(content)) return "";
2165
+ if (typeof content === "string") return capTail(content);
2166
+ if (!Array.isArray(content)) return { text: "", truncated: false };
1427
2167
  const parts = content.flatMap((c) => {
1428
2168
  if (typeof c !== "object" || c === null) return [];
1429
2169
  const { type, text } = c;
1430
2170
  return type === "text" && typeof text === "string" ? [text] : [];
1431
2171
  });
1432
- return capText(parts.join(""));
2172
+ return capTail(parts.join(""));
1433
2173
  }
1434
- return "";
2174
+ return { text: "", truncated: false };
1435
2175
  }
1436
2176
  function lastAssistantError(event) {
1437
2177
  const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
@@ -1446,6 +2186,13 @@ function lastAssistantError(event) {
1446
2186
  }
1447
2187
  return void 0;
1448
2188
  }
2189
+ function capTail(text) {
2190
+ if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return { text, truncated: false };
2191
+ const buf = Buffer.from(text, "utf8");
2192
+ const tail = buf.subarray(buf.length - LAST_TEXT_MAX_BYTES).toString("utf8").replace(/^\uFFFD/, "");
2193
+ return { text: `${LAST_TEXT_TRUNCATED}
2194
+ ${tail}`, truncated: true };
2195
+ }
1449
2196
  function capText(text) {
1450
2197
  if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return text;
1451
2198
  const cut = Buffer.from(text, "utf8").subarray(0, LAST_TEXT_MAX_BYTES).toString("utf8");
@@ -1524,38 +2271,15 @@ function reasonOf(event) {
1524
2271
  const r = typeof event === "object" && event !== null ? event.reason : void 0;
1525
2272
  return typeof r === "string" ? r : void 0;
1526
2273
  }
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
2274
  function muPi(pi, run = defaultRunner()) {
1549
2275
  const delegates = registerDelegate(pi, run);
1550
- registerPrompts(pi);
1551
2276
  const nested = serveCtl(pi);
1552
2277
  const hold = withQueuedInput(
1553
- () => delegates.answerPending() ? "answer" : isUiBlocked() ? "blocked" : void 0
2278
+ () => delegates.answerPending() ? "wake" : isUiBlocked() ? "blocked" : void 0
1554
2279
  );
1555
2280
  const settle = parallelSettle(pi, hold);
1556
- registerNudge(settle.api, run, void 0, nested, hold);
2281
+ registerNudge(settle.api, run, void 0, nested, hold, delegates.delivered);
1557
2282
  registerCloseNudge(settle.api, run, process.env, nested, hold);
1558
- registerRefuteNudge(settle.api, run, nested, hold);
1559
2283
  settle.flush();
1560
2284
  }
1561
2285
  function serveCtl(pi) {
@@ -1570,25 +2294,42 @@ function serveCtl(pi) {
1570
2294
  const existing = map.get(sockPath);
1571
2295
  const g = existing ?? {
1572
2296
  conns: /* @__PURE__ */ new Set(),
2297
+ incarnation: randomUUID(),
2298
+ accepted: /* @__PURE__ */ new Map(),
1573
2299
  waiters: /* @__PURE__ */ new Set(),
1574
2300
  state: "idle",
1575
2301
  since: Date.now(),
1576
2302
  runs: 0,
2303
+ tools: /* @__PURE__ */ new Map(),
1577
2304
  lastText: "",
1578
2305
  pi,
1579
2306
  handle
1580
2307
  };
1581
2308
  g.pi = pi;
1582
2309
  g.handle = handle;
2310
+ g.incarnation ??= randomUUID();
2311
+ g.accepted ??= /* @__PURE__ */ new Map();
2312
+ g.tools ??= /* @__PURE__ */ new Map();
1583
2313
  map.set(sockPath, g);
1584
2314
  const status = () => ({
2315
+ incarnation: g.incarnation,
1585
2316
  state: g.state,
1586
2317
  since: g.since,
1587
2318
  runs: g.runs,
1588
2319
  pending: g.ctx?.hasPendingMessages() ?? false,
2320
+ tool: [...g.tools.values()].at(-1) ?? null,
1589
2321
  ...sessionFileOf(g.ctx),
1590
2322
  ...blockedField()
1591
2323
  });
2324
+ const remember = (id, runs) => {
2325
+ if (id === void 0) return;
2326
+ g.accepted.delete(id);
2327
+ g.accepted.set(id, { runs, at: Date.now() });
2328
+ for (const old of g.accepted.keys()) {
2329
+ if (g.accepted.size <= CTL_MSGID_MEMORY) break;
2330
+ g.accepted.delete(old);
2331
+ }
2332
+ };
1592
2333
  const blockedField = () => {
1593
2334
  const b = uiBlocked();
1594
2335
  return b ? { blocked: b } : {};
@@ -1614,8 +2355,20 @@ function serveCtl(pi) {
1614
2355
  g.since = Date.now();
1615
2356
  if (g.fresh?.armed) settleFresh({ v: V, ok: true, ...status() });
1616
2357
  });
2358
+ pi.on("tool_execution_start", (e) => {
2359
+ const { toolCallId, toolName } = e ?? {};
2360
+ if (typeof toolCallId === "string" && typeof toolName === "string") {
2361
+ g.tools.set(toolCallId, { name: toolName, since: Date.now() });
2362
+ }
2363
+ });
2364
+ pi.on("tool_execution_end", (e) => {
2365
+ g.tools.delete(String(e?.toolCallId));
2366
+ });
1617
2367
  pi.on("agent_end", (e) => {
1618
- g.endText = lastAssistantText(e);
2368
+ g.tools.clear();
2369
+ const cut = lastAssistantText(e);
2370
+ g.endText = cut.text;
2371
+ g.endTextTruncated = cut.truncated;
1619
2372
  g.endError = lastAssistantError(e);
1620
2373
  });
1621
2374
  function sessionFileOf(c) {
@@ -1630,9 +2383,12 @@ function serveCtl(pi) {
1630
2383
  g.ctx = c;
1631
2384
  g.state = "idle";
1632
2385
  g.since = Date.now();
2386
+ g.tools.clear();
1633
2387
  g.runs++;
1634
2388
  g.lastText = g.endText ?? "";
2389
+ g.lastTextTruncated = g.endTextTruncated === true;
1635
2390
  g.endText = void 0;
2391
+ g.endTextTruncated = void 0;
1636
2392
  g.lastError = g.endError;
1637
2393
  g.endError = void 0;
1638
2394
  g.sessionFile = sessionFileOf(c).sessionFile;
@@ -1693,6 +2449,8 @@ function serveCtl(pi) {
1693
2449
  });
1694
2450
  }
1695
2451
  function command(req) {
2452
+ const stale = incarnationRefusal(req);
2453
+ if (stale) return stale;
1696
2454
  const name = CTL_COMMANDS.find((n) => n === req.name);
1697
2455
  if (name === void 0) {
1698
2456
  return fail(`command must be one of ${CTL_COMMANDS.join(", ")}`);
@@ -1718,8 +2476,12 @@ function serveCtl(pi) {
1718
2476
  });
1719
2477
  }
1720
2478
  function fresh(req) {
2479
+ const stale = incarnationRefusal(req, "expectIncarnation");
2480
+ if (stale) return stale;
1721
2481
  const text = str(req, "text");
1722
2482
  if (text === void 0) return fail("fresh needs a string text");
2483
+ const id = msgIdOf(req);
2484
+ if (id.error) return fail(id.error);
1723
2485
  if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
1724
2486
  const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
1725
2487
  if (busy && req.force !== true) return fail("busy");
@@ -1730,6 +2492,7 @@ function serveCtl(pi) {
1730
2492
  armed: false,
1731
2493
  settle: (r) => {
1732
2494
  clearTimeout(timer);
2495
+ if (r.ok) remember(id.id, r.runs ?? g.runs);
1733
2496
  resolve(r);
1734
2497
  }
1735
2498
  };
@@ -1738,6 +2501,17 @@ function serveCtl(pi) {
1738
2501
  ).catch((e) => settleFresh(fail(e instanceof Error ? e.message : String(e))));
1739
2502
  });
1740
2503
  }
2504
+ function incarnationRefusal(req, key = "expectIncarnation") {
2505
+ if (!(key in req) || req[key] === g.incarnation) return void 0;
2506
+ return { ...fail(INCARNATION_MISMATCH), incarnation: g.incarnation };
2507
+ }
2508
+ function msgIdOf(req) {
2509
+ if (!("msgId" in req)) return {};
2510
+ const id = req.msgId;
2511
+ if (typeof id === "string" && id.length >= 1 && id.length <= CTL_MSGID_MAX_LENGTH)
2512
+ return { id };
2513
+ return { error: `msgId must be a string of 1-${CTL_MSGID_MAX_LENGTH} characters` };
2514
+ }
1741
2515
  async function handle(req, conn) {
1742
2516
  switch (req.op) {
1743
2517
  case "hello":
@@ -1747,18 +2521,31 @@ function serveCtl(pi) {
1747
2521
  agent: process.env.MU_AGENT_NAME,
1748
2522
  workstream: process.env.MU_WORKSTREAM,
1749
2523
  piVersion,
2524
+ incarnation: g.incarnation,
1750
2525
  ops: [...CTL_OPS],
2526
+ features: [...CTL_FEATURES],
1751
2527
  ...EXT_VERSION !== void 0 ? { extVersion: EXT_VERSION } : {}
1752
2528
  };
1753
- case "status":
1754
- return { v: V, ok: true, ...status() };
2529
+ case "status": {
2530
+ const id = msgIdOf(req);
2531
+ if (id.error) return fail(id.error);
2532
+ if (id.id === void 0) return { v: V, ok: true, ...status() };
2533
+ const got = g.accepted.get(id.id);
2534
+ const msg = got ? { id: id.id, accepted: true, ...got } : { id: id.id, accepted: false };
2535
+ return { v: V, ok: true, ...status(), msg };
2536
+ }
1755
2537
  case "send": {
2538
+ const stale = incarnationRefusal(req);
2539
+ if (stale) return stale;
1756
2540
  const text = str(req, "text");
1757
2541
  if (text === void 0) return fail("send needs a string text");
2542
+ const id = msgIdOf(req);
2543
+ if (id.error) return fail(id.error);
1758
2544
  const mode = req.mode === "steer" ? "steer" : "followUp";
1759
2545
  const before = status();
1760
2546
  if (g.ctx?.isIdle() ?? g.state === "idle") await g.pi.sendUserMessage(text);
1761
2547
  else await g.pi.sendUserMessage(text, { deliverAs: mode });
2548
+ remember(id.id, before.runs);
1762
2549
  return { v: V, ok: true, ...before };
1763
2550
  }
1764
2551
  case "fresh":
@@ -1766,6 +2553,9 @@ function serveCtl(pi) {
1766
2553
  case "command":
1767
2554
  return command(req);
1768
2555
  case "wait":
2556
+ if ("incarnation" in req && req.incarnation !== g.incarnation) {
2557
+ return { ...fail(INCARNATION_MISMATCH), incarnation: g.incarnation };
2558
+ }
1769
2559
  return wait(
1770
2560
  num(req, "afterRuns"),
1771
2561
  num(req, "timeoutMs"),
@@ -1786,8 +2576,12 @@ function serveCtl(pi) {
1786
2576
  }
1787
2577
  }
1788
2578
  async function interrupt(req, conn) {
2579
+ const stale = incarnationRefusal(req);
2580
+ if (stale) return stale;
1789
2581
  const text = str(req, "text");
1790
2582
  if (text === void 0) return fail("interrupt needs a string text");
2583
+ const id = msgIdOf(req);
2584
+ if (id.error) return fail(id.error);
1791
2585
  const wasBusy = !(g.ctx?.isIdle() ?? g.state === "idle");
1792
2586
  const { pending } = status();
1793
2587
  if (wasBusy) {
@@ -1799,10 +2593,12 @@ function serveCtl(pi) {
1799
2593
  }
1800
2594
  const before = status();
1801
2595
  await g.pi.sendUserMessage(text);
2596
+ remember(id.id, before.runs);
1802
2597
  return { v: V, ok: true, ...before, pending, wasBusy };
1803
2598
  }
1804
2599
  const settledRun = () => ({
1805
2600
  lastText: g.lastText,
2601
+ ...g.lastTextTruncated === true ? { lastTextTruncated: true } : {},
1806
2602
  ...g.lastError !== void 0 ? { lastError: g.lastError } : {},
1807
2603
  ...g.sessionFile !== void 0 ? { sessionFile: g.sessionFile } : {}
1808
2604
  });
@@ -1926,8 +2722,8 @@ function serveCtl(pi) {
1926
2722
  await bind();
1927
2723
  });
1928
2724
  pi.on("session_shutdown", async (e) => {
1929
- const reason = reasonOf(e);
1930
- if (reason === "new" || reason === "resume" || reason === "fork" || reason === "reload") {
2725
+ const reason2 = reasonOf(e);
2726
+ if (reason2 === "new" || reason2 === "resume" || reason2 === "fork" || reason2 === "reload") {
1931
2727
  return;
1932
2728
  }
1933
2729
  const s = g.server;
@@ -1957,7 +2753,6 @@ export {
1957
2753
  isUiBlocked,
1958
2754
  lastAssistantError,
1959
2755
  lastAssistantText,
1960
- promptsDir,
1961
2756
  uiBlocked
1962
2757
  };
1963
2758
  //# sourceMappingURL=mu-pi.js.map