@mu-crew/mu 3.1.0 → 3.2.1

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 (43) hide show
  1. package/AGENTS.md +203 -588
  2. package/README.md +80 -308
  3. package/dist/cli.js +6106 -4716
  4. package/dist/cli.js.map +1 -1
  5. package/dist/extension/mu-pi.js +786 -0
  6. package/dist/extension/mu-pi.js.map +1 -0
  7. package/dist/index.d.ts +738 -536
  8. package/dist/index.js +6617 -5785
  9. package/dist/index.js.map +1 -1
  10. package/docs/ARCHITECTURE.md +178 -1008
  11. package/docs/ROADMAP.md +88 -169
  12. package/docs/VISION.md +206 -407
  13. package/docs/VOCABULARY.md +202 -455
  14. package/docs/architecture/control-socket.md +119 -0
  15. package/docs/architecture/dag.md +113 -0
  16. package/docs/architecture/mux.md +163 -0
  17. package/docs/architecture/ops-log.md +140 -0
  18. package/docs/architecture/sdk.md +185 -0
  19. package/docs/architecture/sync.md +129 -0
  20. package/docs/architecture/tui.md +146 -0
  21. package/docs/guide/README.md +25 -0
  22. package/docs/guide/backends.md +61 -0
  23. package/docs/guide/cleanup.md +74 -0
  24. package/docs/guide/delegate.md +87 -0
  25. package/docs/guide/dispatch.md +137 -0
  26. package/docs/guide/getting-started.md +177 -0
  27. package/docs/guide/recovery.md +133 -0
  28. package/docs/guide/remote.md +74 -0
  29. package/docs/guide/sql.md +76 -0
  30. package/docs/guide/stop-a-worker.md +64 -0
  31. package/docs/guide/sync.md +78 -0
  32. package/docs/guide/tui.md +98 -0
  33. package/docs/guide/upgrade.md +76 -0
  34. package/docs/history/CHANGELOG-pre-3.md +4866 -0
  35. package/docs/reference/env.md +87 -0
  36. package/docs/reference/naming.md +82 -0
  37. package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
  38. package/docs/specs/2026-09-29-task-substates.md +1 -1
  39. package/package.json +4 -4
  40. package/skills/mu/REMOTE_WORKERS.md +186 -491
  41. package/skills/mu/SKILL.md +142 -217
  42. package/docs/HANDOVER.md +0 -486
  43. package/docs/USAGE_GUIDE.md +0 -2637
@@ -0,0 +1,786 @@
1
+ // extension/mu-pi.ts
2
+ import {
3
+ chmodSync,
4
+ existsSync as existsSync2,
5
+ linkSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ rmSync,
10
+ statSync as statSync2
11
+ } from "fs";
12
+ import { connect, createServer } from "net";
13
+ import { dirname, join as join2 } from "path";
14
+
15
+ // src/ctl/protocol.ts
16
+ var CTL_PROTOCOL_VERSION = 1;
17
+ var CTL_OPS = [
18
+ "hello",
19
+ "status",
20
+ "send",
21
+ "wait",
22
+ "abort",
23
+ "fresh"
24
+ ];
25
+ var UNKNOWN_OP_PREFIX = "unknown op: ";
26
+ function encode(msg) {
27
+ return `${JSON.stringify(msg)}
28
+ `;
29
+ }
30
+ var LineDecoder = class {
31
+ buf = "";
32
+ push(chunk) {
33
+ this.buf += chunk;
34
+ const parts = this.buf.split("\n");
35
+ this.buf = parts.pop() ?? "";
36
+ return parts;
37
+ }
38
+ };
39
+
40
+ // extension/delegate.ts
41
+ import { execFile } from "child_process";
42
+ import { existsSync, statSync } from "fs";
43
+ import { delimiter, join } from "path";
44
+ import { fileURLToPath } from "url";
45
+ var DELEGATE_TOOL = "mu_delegate";
46
+ var DELEGATE_CANCEL_TOOL = "mu_delegate_cancel";
47
+ var DELEGATE_MESSAGE_TYPE = "mu-delegate";
48
+ var DELEGATE_WORKSTREAM = "scratch";
49
+ var DELEGATE_STATUS_KEY = "mu-delegate";
50
+ function delegateStatus(n) {
51
+ return n > 0 ? `${n} delegate${n === 1 ? "" : "s"} running` : void 0;
52
+ }
53
+ var DELEGATE_TIMEOUT_S = 3600;
54
+ var W = DELEGATE_WORKSTREAM;
55
+ function delegateEnabled(env = process.env) {
56
+ if (env.MU_MANAGED_AGENT) return false;
57
+ if (env.MU_DELEGATE === "0") return false;
58
+ if (env.MU_MUX || env.HERDR_ENV === "1" || env.TMUX || env.TMUX_PANE) return true;
59
+ return onPath("tmux", env) || onPath("herdr", env);
60
+ }
61
+ function isDir(p) {
62
+ try {
63
+ return statSync(p).isDirectory();
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ function onPath(bin, env) {
69
+ return (env.PATH ?? "").split(delimiter).some((d) => d !== "" && existsSync(join(d, bin)));
70
+ }
71
+ function defaultRunner() {
72
+ let cli;
73
+ try {
74
+ const p = fileURLToPath(new URL("../cli.js", import.meta.url));
75
+ if (existsSync(p)) cli = p;
76
+ } catch {
77
+ }
78
+ return (args, signal) => new Promise((resolve) => {
79
+ const [cmd, argv] = cli ? [process.execPath, [cli, ...args]] : ["mu", [...args]];
80
+ execFile(
81
+ cmd,
82
+ argv,
83
+ { maxBuffer: 16 * 1024 * 1024, ...signal ? { signal } : {} },
84
+ (err, stdout, stderr) => {
85
+ const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
86
+ resolve({ code, stdout: String(stdout), stderr: String(stderr || err?.message || "") });
87
+ }
88
+ );
89
+ });
90
+ }
91
+ function formatElapsed(ms) {
92
+ const t = Math.max(0, Math.round(ms / 1e3));
93
+ if (t < 60) return `${t}s`;
94
+ const m = Math.floor(t / 60);
95
+ if (m < 60) return `${m}m ${String(t % 60).padStart(2, "0")}s`;
96
+ return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
97
+ }
98
+ function delegateMessage(name, agent, pane, x = {}) {
99
+ const timeoutS = x.timeoutS ?? DELEGATE_TIMEOUT_S;
100
+ const who = `Delegate ${W}/${name}`;
101
+ const took = x.elapsedMs !== void 0 ? ` after ${formatElapsed(x.elapsedMs)}` : "";
102
+ const fate = pane.closed ? "Pane closed." : `Pane kept (${pane.why}).`;
103
+ const look = [
104
+ x.attach ? `Attach: ${x.attach}` : void 0,
105
+ `Read: mu agent read ${name} -n 50 -w ${W}`
106
+ ].filter((v) => v !== void 0);
107
+ const tailBlock = x.tail?.trim() ? `
108
+
109
+ Pane tail:
110
+ ${x.tail.trimEnd()}` : "";
111
+ const ws = x.workspace ? ` Workspace: ${x.workspace}` : "";
112
+ switch (agent?.outcome) {
113
+ case "done":
114
+ return `${who} finished${took}. ${fate}${ws}
115
+
116
+ ${agent.lastText ?? ""}`;
117
+ case "empty":
118
+ return `${who} finished${took} without a text answer. ${fate}${ws}${tailBlock}`;
119
+ case "died":
120
+ return `${who} died${took} before answering. ${fate}${ws}${tailBlock}`;
121
+ case "timeout":
122
+ case "pending":
123
+ return [
124
+ `${who} is still running after ${timeoutS}s; stopped waiting, so its answer will not arrive here. ${fate}${ws}`,
125
+ ...look,
126
+ `Wait again: mu agent wait ${name} -w ${W} --json`
127
+ ].join("\n");
128
+ default:
129
+ return `${who}: mu agent wait gave no result. ${fate}${ws}${tailBlock}`;
130
+ }
131
+ }
132
+ function labelStem(label) {
133
+ if (typeof label !== "string") return void 0;
134
+ const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "");
135
+ return stem || void 0;
136
+ }
137
+ function registerDelegate(pi, run = defaultRunner()) {
138
+ if (!delegateEnabled()) return;
139
+ const inflight = /* @__PURE__ */ new Map();
140
+ const reserved = /* @__PURE__ */ new Set();
141
+ let ui;
142
+ const showStatus = () => {
143
+ try {
144
+ ui?.setStatus?.(DELEGATE_STATUS_KEY, delegateStatus(inflight.size));
145
+ } catch {
146
+ }
147
+ };
148
+ const track = (ctx) => {
149
+ if (ctx?.hasUI !== false && ctx?.ui) ui = ctx.ui;
150
+ };
151
+ const forget = (name) => {
152
+ inflight.delete(name);
153
+ reserved.delete(name);
154
+ showStatus();
155
+ };
156
+ const mu = run;
157
+ const json = (r) => {
158
+ try {
159
+ const v = JSON.parse(r.stdout);
160
+ return typeof v === "object" && v !== null ? v : void 0;
161
+ } catch {
162
+ return void 0;
163
+ }
164
+ };
165
+ const failure = (what, r) => new Error(`${what} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
166
+ async function pickName(stem) {
167
+ const r = await mu(["agent", "list", "-w", W, "--json"]);
168
+ const agents = json(r)?.agents;
169
+ const taken = new Set(
170
+ Array.isArray(agents) ? agents.map((a) => a.name).filter((n) => n) : []
171
+ );
172
+ for (let n = 1; ; n++) {
173
+ const name = stem ? n === 1 ? `delegate-${stem}` : `delegate-${stem}-${n}` : `delegate-${n}`;
174
+ if (!taken.has(name) && !reserved.has(name)) {
175
+ reserved.add(name);
176
+ return name;
177
+ }
178
+ }
179
+ }
180
+ async function spawn(params, cwd, signal) {
181
+ const stem = labelStem(params.label);
182
+ for (let attempt = 0; ; attempt++) {
183
+ signal?.throwIfAborted();
184
+ const name = await pickName(stem);
185
+ const args = ["agent", "spawn", name, "-w", W, "--json"];
186
+ if (params.workspace === true) args.push("--workspace");
187
+ else if (cwd) args.push("--cwd", cwd);
188
+ if (typeof params.cli === "string" && params.cli) args.push("--cli", params.cli);
189
+ const r = await mu(args);
190
+ if (signal?.aborted && r.code === 0) {
191
+ reserved.delete(name);
192
+ await mu(["agent", "close", name, "-w", W]);
193
+ signal.throwIfAborted();
194
+ }
195
+ if (r.code !== 0 && /already exists/.test(r.stderr) && attempt < 3) {
196
+ reserved.delete(name);
197
+ continue;
198
+ }
199
+ if (r.code !== 0) {
200
+ reserved.delete(name);
201
+ throw failure("mu agent spawn", r);
202
+ }
203
+ const out = json(r) ?? {};
204
+ if (out.ctl !== "ok") {
205
+ reserved.delete(name);
206
+ throw new Error(
207
+ `${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}`
208
+ );
209
+ }
210
+ const steps = Array.isArray(out.nextSteps) ? out.nextSteps : [];
211
+ const attach = steps.map((s) => s).find((s) => s.intent === "Attach the pane")?.command;
212
+ const wsPath = out.workspace?.path;
213
+ return {
214
+ name,
215
+ ...typeof attach === "string" ? { attach } : {},
216
+ ...typeof wsPath === "string" ? { workspace: wsPath } : {}
217
+ };
218
+ }
219
+ }
220
+ async function deliver(name, wait, keep, extras) {
221
+ const agent = json(wait)?.agents?.[0];
222
+ const outcome = agent?.outcome;
223
+ const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
224
+ let pane = { closed: false, why: "keep: true" };
225
+ if (outcome !== "done" && outcome !== "empty")
226
+ pane = { closed: false, why: "kept as evidence" };
227
+ else if (!keep) {
228
+ const c = await mu(["agent", "close", name, "-w", W]);
229
+ pane = c.code === 0 ? { closed: true } : { closed: false, why: `close refused: ${(c.stderr || c.stdout).trim()}` };
230
+ }
231
+ await pi.sendMessage(
232
+ {
233
+ customType: DELEGATE_MESSAGE_TYPE,
234
+ content: delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} }),
235
+ display: true,
236
+ details: {
237
+ name,
238
+ workstream: W,
239
+ outcome: outcome ?? null,
240
+ closed: pane.closed,
241
+ answer: outcome === "done" ? agent?.lastText ?? null : null,
242
+ elapsedMs: extras.elapsedMs ?? null,
243
+ workspace: extras.workspace ?? null
244
+ }
245
+ },
246
+ { deliverAs: "followUp", triggerTurn: true }
247
+ );
248
+ }
249
+ pi.registerTool({
250
+ name: DELEGATE_TOOL,
251
+ label: "mu delegate",
252
+ description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation; call it several times to fan out in parallel. The subagent starts with no context, in your working directory: put everything it needs in task. The call returns at once and the answer arrives later as a follow-up message: carry on with other work, or end your turn if your next step needs the answer (the follow-up resumes you). The pane closes after a clean finish; keep: true leaves it open.`,
253
+ promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
254
+ parameters: {
255
+ type: "object",
256
+ properties: {
257
+ task: { type: "string", description: "The complete task, with all context it needs" },
258
+ label: {
259
+ type: "string",
260
+ description: "Short label naming the delegate, e.g. 'review' \u2192 delegate-review. Use one per subtask when fanning out."
261
+ },
262
+ brief: {
263
+ type: "string",
264
+ description: "Optional standing role or rules, sent before the task, e.g. 'You are a reviewer; read only.' The rules are instructions, not enforced. The job itself goes in task."
265
+ },
266
+ cwd: {
267
+ type: "string",
268
+ description: "Working directory for the delegate (default: yours; ignored with workspace)"
269
+ },
270
+ timeout: {
271
+ type: "number",
272
+ exclusiveMinimum: 0,
273
+ 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.`
274
+ },
275
+ workspace: {
276
+ type: "boolean",
277
+ 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."
278
+ },
279
+ cli: {
280
+ type: "string",
281
+ description: "Agent CLI key (default pi). Name a configured key such as 'pi_fast' to run a cheaper model."
282
+ },
283
+ keep: {
284
+ type: "boolean",
285
+ description: "Keep the pane after it finishes, to talk to it again"
286
+ }
287
+ },
288
+ required: ["task"],
289
+ additionalProperties: false
290
+ },
291
+ async execute(_id, params, signal, _onUpdate, ctx) {
292
+ track(ctx);
293
+ const task = typeof params.task === "string" ? params.task.trim() : "";
294
+ if (!task) throw new Error("mu_delegate needs a non-empty task");
295
+ const brief = typeof params.brief === "string" ? params.brief.trim() : "";
296
+ const keep = params.keep === true;
297
+ const t = params.timeout;
298
+ if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
299
+ throw new Error(
300
+ `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
301
+ );
302
+ const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
303
+ let cwd;
304
+ if (params.workspace !== true) {
305
+ cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
306
+ if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
307
+ }
308
+ const { name, attach, workspace } = await spawn(params, cwd, signal);
309
+ const entry = { abort: new AbortController(), cancelling: false };
310
+ inflight.set(name, entry);
311
+ showStatus();
312
+ const started = Date.now();
313
+ const waiting = mu(
314
+ ["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
315
+ entry.abort.signal
316
+ );
317
+ const sent = await mu(["agent", "send", name, brief ? `${brief}
318
+
319
+ ${task}` : task, "-w", W]);
320
+ if (sent.code !== 0) {
321
+ entry.abort.abort();
322
+ forget(name);
323
+ const c = await mu(["agent", "close", name, "-w", W]);
324
+ const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
325
+ throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
326
+ }
327
+ const settle = async (r) => {
328
+ forget(name);
329
+ try {
330
+ await deliver(name, r, keep, {
331
+ timeoutS,
332
+ elapsedMs: Date.now() - started,
333
+ ...attach ? { attach } : {},
334
+ ...workspace ? { workspace } : {}
335
+ });
336
+ } catch (e) {
337
+ await Promise.resolve(
338
+ pi.sendMessage(
339
+ {
340
+ customType: DELEGATE_MESSAGE_TYPE,
341
+ 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}`,
342
+ display: true,
343
+ details: { name, workstream: W, outcome: null, closed: false }
344
+ },
345
+ { deliverAs: "followUp", triggerTurn: true }
346
+ )
347
+ ).catch(() => {
348
+ });
349
+ }
350
+ };
351
+ void waiting.then(async (r) => {
352
+ if (inflight.get(name) !== entry) return;
353
+ if (entry.cancelling) entry.parked = () => settle(r);
354
+ else await settle(r);
355
+ });
356
+ const lines = [
357
+ `Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
358
+ workspace ? `Workspace: ${workspace}` : void 0,
359
+ attach ? `Attach: ${attach}` : void 0,
360
+ `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
361
+ ].filter((x) => x !== void 0);
362
+ return {
363
+ content: [{ type: "text", text: lines.join("\n") }],
364
+ details: {
365
+ name,
366
+ workstream: W,
367
+ attach: attach ?? null,
368
+ workspace: workspace ?? null,
369
+ keep
370
+ }
371
+ };
372
+ }
373
+ });
374
+ pi.registerTool({
375
+ name: DELEGATE_CANCEL_TOOL,
376
+ label: "mu delegate cancel",
377
+ 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.`,
378
+ parameters: {
379
+ type: "object",
380
+ properties: {
381
+ name: { type: "string", description: "Delegate name, e.g. delegate-1" },
382
+ keep: { type: "boolean", description: "Leave the pane open after aborting" }
383
+ },
384
+ required: ["name"],
385
+ additionalProperties: false
386
+ },
387
+ async execute(_id, params, _signal, _onUpdate, ctx) {
388
+ track(ctx);
389
+ const name = typeof params.name === "string" ? params.name : "";
390
+ if (!name) throw new Error("mu_delegate_cancel needs a name");
391
+ const entry = inflight.get(name);
392
+ if (entry) entry.cancelling = true;
393
+ const ab = await mu(["agent", "abort", name, "-w", W]);
394
+ if (ab.code !== 0) {
395
+ if (entry) {
396
+ entry.cancelling = false;
397
+ const parked = entry.parked;
398
+ entry.parked = void 0;
399
+ if (parked) void parked();
400
+ }
401
+ throw failure(`mu agent abort ${W}/${name}`, ab);
402
+ }
403
+ if (entry) {
404
+ entry.abort.abort();
405
+ forget(name);
406
+ }
407
+ let text = `Aborted ${W}/${name}.`;
408
+ if (params.keep === true) text += " Pane kept.";
409
+ else {
410
+ const c = await mu(["agent", "close", name, "-w", W]);
411
+ text += c.code === 0 ? " Pane closed." : ` Pane kept: close refused: ${(c.stderr || c.stdout).trim()}`;
412
+ }
413
+ return { content: [{ type: "text", text }], details: { name, workstream: W } };
414
+ }
415
+ });
416
+ pi.on("session_shutdown", (_e, ctx) => {
417
+ if (inflight.size === 0) return;
418
+ const names = [...inflight.keys()];
419
+ for (const e of inflight.values()) e.abort.abort();
420
+ inflight.clear();
421
+ showStatus();
422
+ ui = void 0;
423
+ const msg = `${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`;
424
+ if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
425
+ else process.stderr.write(`mu: ${msg}
426
+ `);
427
+ });
428
+ }
429
+
430
+ // extension/mu-pi.ts
431
+ var EXT_VERSION = true ? "3.2.1" : void 0;
432
+ var FRESH_COMMAND = "mu-fresh";
433
+ var FRESH_TIMEOUT_MS = 3e4;
434
+ var LAST_TEXT_MAX_BYTES = 64 * 1024;
435
+ var LAST_TEXT_TRUNCATED = "[truncated, see pane]";
436
+ var LIVE_PROBE_MS = 500;
437
+ var V = CTL_PROTOCOL_VERSION;
438
+ function fail(error) {
439
+ return { v: V, ok: false, error };
440
+ }
441
+ function detectPiVersion() {
442
+ try {
443
+ const entry = process.argv[1];
444
+ if (!entry) return void 0;
445
+ let dir = dirname(realpathSync(entry));
446
+ for (let i = 0; i < 6; i++) {
447
+ const pkgPath = join2(dir, "package.json");
448
+ if (existsSync2(pkgPath)) {
449
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
450
+ if (typeof pkg === "object" && pkg !== null) {
451
+ const { name, version } = pkg;
452
+ if (typeof name === "string" && /pi/.test(name) && typeof version === "string") {
453
+ return version;
454
+ }
455
+ }
456
+ }
457
+ const up = dirname(dir);
458
+ if (up === dir) break;
459
+ dir = up;
460
+ }
461
+ } catch {
462
+ }
463
+ return void 0;
464
+ }
465
+ function str(o, k) {
466
+ const x = o[k];
467
+ return typeof x === "string" ? x : void 0;
468
+ }
469
+ function num(o, k) {
470
+ const x = o[k];
471
+ return typeof x === "number" && Number.isFinite(x) ? x : void 0;
472
+ }
473
+ var SHARED = /* @__PURE__ */ Symbol.for("mu.pi.ctl");
474
+ function sharedMap() {
475
+ const g = globalThis;
476
+ g[SHARED] ??= /* @__PURE__ */ new Map();
477
+ return g[SHARED];
478
+ }
479
+ function lastAssistantText(event) {
480
+ const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
481
+ if (!Array.isArray(msgs)) return "";
482
+ for (let i = msgs.length - 1; i >= 0; i--) {
483
+ const m = msgs[i];
484
+ if (typeof m !== "object" || m === null) continue;
485
+ const { role, content } = m;
486
+ if (role !== "assistant") continue;
487
+ if (typeof content === "string") return capText(content);
488
+ if (!Array.isArray(content)) return "";
489
+ const parts = content.flatMap((c) => {
490
+ if (typeof c !== "object" || c === null) return [];
491
+ const { type, text } = c;
492
+ return type === "text" && typeof text === "string" ? [text] : [];
493
+ });
494
+ return capText(parts.join(""));
495
+ }
496
+ return "";
497
+ }
498
+ function capText(text) {
499
+ if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return text;
500
+ const cut = Buffer.from(text, "utf8").subarray(0, LAST_TEXT_MAX_BYTES).toString("utf8");
501
+ return `${cut.replace(/\uFFFD$/, "")}
502
+ ${LAST_TEXT_TRUNCATED}`;
503
+ }
504
+ function inodeOf(path) {
505
+ try {
506
+ return statSync2(path).ino;
507
+ } catch {
508
+ return void 0;
509
+ }
510
+ }
511
+ function socketIsLive(path) {
512
+ return new Promise((resolve) => {
513
+ const c = connect(path);
514
+ const done = (live) => {
515
+ clearTimeout(timer);
516
+ c.destroy();
517
+ resolve(live);
518
+ };
519
+ const timer = setTimeout(() => done(true), LIVE_PROBE_MS);
520
+ c.once("connect", () => done(true));
521
+ c.once(
522
+ "error",
523
+ (e) => done(e.code !== "ENOENT" && e.code !== "ECONNREFUSED")
524
+ );
525
+ });
526
+ }
527
+ var bindSeq = 0;
528
+ function reasonOf(event) {
529
+ const r = typeof event === "object" && event !== null ? event.reason : void 0;
530
+ return typeof r === "string" ? r : void 0;
531
+ }
532
+ function muPi(pi) {
533
+ registerDelegate(pi);
534
+ serveCtl(pi);
535
+ }
536
+ function serveCtl(pi) {
537
+ const sock = process.env.MU_CTL_SOCK;
538
+ if (!sock) return;
539
+ const sockPath = sock;
540
+ const piVersion = detectPiVersion();
541
+ const map = sharedMap();
542
+ const existing = map.get(sockPath);
543
+ const g = existing ?? {
544
+ conns: /* @__PURE__ */ new Set(),
545
+ waiters: /* @__PURE__ */ new Set(),
546
+ state: "idle",
547
+ since: Date.now(),
548
+ runs: 0,
549
+ lastText: "",
550
+ pi,
551
+ handle
552
+ };
553
+ g.pi = pi;
554
+ g.handle = handle;
555
+ map.set(sockPath, g);
556
+ const status = () => ({
557
+ state: g.state,
558
+ since: g.since,
559
+ runs: g.runs,
560
+ pending: g.ctx?.hasPendingMessages() ?? false
561
+ });
562
+ const settleFresh = (r) => {
563
+ const f = g.fresh;
564
+ if (!f) return;
565
+ g.fresh = void 0;
566
+ f.settle(r);
567
+ };
568
+ pi.on("agent_start", (_e, c) => {
569
+ g.ctx = c;
570
+ g.state = "busy";
571
+ g.since = Date.now();
572
+ if (g.fresh?.armed) settleFresh({ v: V, ok: true, ...status() });
573
+ });
574
+ pi.on("agent_end", (e) => {
575
+ g.endText = lastAssistantText(e);
576
+ });
577
+ pi.on("agent_settled", (_e, c) => {
578
+ g.ctx = c;
579
+ g.state = "idle";
580
+ g.since = Date.now();
581
+ g.runs++;
582
+ g.lastText = g.endText ?? "";
583
+ g.endText = void 0;
584
+ for (const w of [...g.waiters]) w();
585
+ });
586
+ pi.registerCommand(FRESH_COMMAND, {
587
+ description: "mu internal: new session + the prompt mu queued (run by mu agent send --fresh)",
588
+ handler: async (_args, ctx) => {
589
+ const f = g.fresh;
590
+ if (!f) return;
591
+ try {
592
+ const r = await ctx.newSession({
593
+ withSession: async (c) => {
594
+ g.ctx = c;
595
+ f.armed = true;
596
+ await c.sendUserMessage(f.text);
597
+ }
598
+ });
599
+ if (r.cancelled) settleFresh(fail("new session was cancelled"));
600
+ } catch (e) {
601
+ settleFresh(fail(e instanceof Error ? e.message : String(e)));
602
+ }
603
+ settleFresh({ v: V, ok: true, ...status() });
604
+ }
605
+ });
606
+ function fresh(req) {
607
+ const text = str(req, "text");
608
+ if (text === void 0) return fail("fresh needs a string text");
609
+ if (g.fresh) return fail("a fresh send is already in progress");
610
+ const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
611
+ if (busy && req.force !== true) return fail("busy");
612
+ return new Promise((resolve) => {
613
+ const timer = setTimeout(() => settleFresh(fail("fresh timed out")), FRESH_TIMEOUT_MS);
614
+ g.fresh = {
615
+ text,
616
+ armed: false,
617
+ settle: (r) => {
618
+ clearTimeout(timer);
619
+ resolve(r);
620
+ }
621
+ };
622
+ Promise.resolve(
623
+ g.pi.sendUserMessage(`/${FRESH_COMMAND}`, { expandPromptTemplates: true })
624
+ ).catch((e) => settleFresh(fail(e instanceof Error ? e.message : String(e))));
625
+ });
626
+ }
627
+ async function handle(req, conn) {
628
+ switch (req.op) {
629
+ case "hello":
630
+ return {
631
+ v: V,
632
+ ok: true,
633
+ agent: process.env.MU_AGENT_NAME,
634
+ workstream: process.env.MU_WORKSTREAM,
635
+ piVersion,
636
+ ops: [...CTL_OPS],
637
+ ...EXT_VERSION !== void 0 ? { extVersion: EXT_VERSION } : {}
638
+ };
639
+ case "status":
640
+ return { v: V, ok: true, ...status() };
641
+ case "send": {
642
+ const text = str(req, "text");
643
+ if (text === void 0) return fail("send needs a string text");
644
+ const mode = req.mode === "steer" ? "steer" : "followUp";
645
+ if (g.ctx?.isIdle() ?? g.state === "idle") await g.pi.sendUserMessage(text);
646
+ else await g.pi.sendUserMessage(text, { deliverAs: mode });
647
+ return { v: V, ok: true, state: g.state };
648
+ }
649
+ case "fresh":
650
+ return fresh(req);
651
+ case "wait":
652
+ return wait(num(req, "afterRuns"), num(req, "timeoutMs"), conn);
653
+ case "abort":
654
+ if (!g.ctx) return fail("no pi context yet");
655
+ g.ctx.abort();
656
+ return { v: V, ok: true, state: g.state };
657
+ default:
658
+ return { ...fail(`${UNKNOWN_OP_PREFIX}${String(req.op)}`), ops: [...CTL_OPS] };
659
+ }
660
+ }
661
+ function wait(afterRuns, timeoutMs, conn) {
662
+ if (afterRuns !== void 0 && g.runs > afterRuns && g.state === "idle") {
663
+ return Promise.resolve({ v: V, ok: true, ...status(), lastText: g.lastText });
664
+ }
665
+ return new Promise((resolve) => {
666
+ let timer;
667
+ const done = (r) => {
668
+ g.waiters.delete(waiter);
669
+ if (timer) clearTimeout(timer);
670
+ conn.off("close", onClose);
671
+ resolve(r);
672
+ };
673
+ const waiter = () => done({ v: V, ok: true, ...status(), lastText: g.lastText });
674
+ const onClose = () => done(fail("client closed"));
675
+ g.waiters.add(waiter);
676
+ conn.on("close", onClose);
677
+ if (timeoutMs !== void 0) timer = setTimeout(() => done(fail("timeout")), timeoutMs);
678
+ });
679
+ }
680
+ function onConnection(conn) {
681
+ g.conns.add(conn);
682
+ conn.setEncoding("utf8");
683
+ const dec = new LineDecoder();
684
+ let seen = false;
685
+ conn.on("close", () => g.conns.delete(conn));
686
+ conn.on("error", () => {
687
+ });
688
+ conn.on("data", (chunk) => {
689
+ const line = dec.push(chunk)[0];
690
+ if (line === void 0 || seen) return;
691
+ seen = true;
692
+ let req;
693
+ try {
694
+ req = JSON.parse(line);
695
+ } catch {
696
+ conn.end(encode(fail("bad json")));
697
+ return;
698
+ }
699
+ if (typeof req !== "object" || req === null) {
700
+ conn.end(encode(fail("request must be an object")));
701
+ return;
702
+ }
703
+ g.handle(req, conn).then(
704
+ (r) => {
705
+ if (!conn.destroyed) conn.end(encode(r));
706
+ },
707
+ (e) => {
708
+ if (!conn.destroyed) conn.end(encode(fail(e instanceof Error ? e.message : String(e))));
709
+ }
710
+ );
711
+ });
712
+ }
713
+ async function bind() {
714
+ mkdirSync(dirname(sockPath), { recursive: true, mode: 448 });
715
+ if (await socketIsLive(sockPath)) {
716
+ process.stderr.write(
717
+ `mu: control socket ${sockPath} is served by another pi; this pi will not take it
718
+ `
719
+ );
720
+ return;
721
+ }
722
+ rmSync(sockPath, { force: true });
723
+ const tmp = join2(dirname(sockPath), `.${process.pid.toString(36)}${(++bindSeq).toString(36)}`);
724
+ rmSync(tmp, { force: true });
725
+ const s = createServer(onConnection);
726
+ await new Promise((resolve, reject) => {
727
+ s.once("error", reject);
728
+ s.listen(tmp, () => {
729
+ s.off("error", reject);
730
+ resolve();
731
+ });
732
+ });
733
+ chmodSync(tmp, 384);
734
+ try {
735
+ linkSync(tmp, sockPath);
736
+ } catch (e) {
737
+ s.close();
738
+ if (e.code !== "EEXIST") throw e;
739
+ process.stderr.write(`mu: control socket ${sockPath} was taken by another pi
740
+ `);
741
+ return;
742
+ } finally {
743
+ rmSync(tmp, { force: true });
744
+ }
745
+ g.server = s;
746
+ g.ino = inodeOf(sockPath);
747
+ }
748
+ pi.on("session_start", async (_e, c) => {
749
+ g.ctx = c;
750
+ const s = g.server;
751
+ if (s) {
752
+ if (g.ino !== void 0 && inodeOf(sockPath) === g.ino) return;
753
+ g.server = void 0;
754
+ g.ino = void 0;
755
+ s.close();
756
+ }
757
+ await bind();
758
+ });
759
+ pi.on("session_shutdown", async (e) => {
760
+ const reason = reasonOf(e);
761
+ if (reason === "new" || reason === "resume" || reason === "fork" || reason === "reload") {
762
+ return;
763
+ }
764
+ const s = g.server;
765
+ const ino = g.ino;
766
+ g.server = void 0;
767
+ g.ino = void 0;
768
+ map.delete(sockPath);
769
+ if (!s) return;
770
+ g.waiters.clear();
771
+ settleFresh(fail("pi is shutting down"));
772
+ for (const c of g.conns) c.destroy();
773
+ await new Promise((resolve) => s.close(() => resolve()));
774
+ if (ino !== void 0 && inodeOf(sockPath) === ino) rmSync(sockPath, { force: true });
775
+ });
776
+ }
777
+ export {
778
+ FRESH_COMMAND,
779
+ FRESH_TIMEOUT_MS,
780
+ LAST_TEXT_MAX_BYTES,
781
+ LAST_TEXT_TRUNCATED,
782
+ LIVE_PROBE_MS,
783
+ muPi as default,
784
+ lastAssistantText
785
+ };
786
+ //# sourceMappingURL=mu-pi.js.map