@mu-crew/mu 3.4.0 → 3.6.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.
- package/dist/cli.js +475 -257
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +308 -107
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/docs/VISION.md +3 -1
- package/docs/architecture/ops-log.md +26 -7
- package/docs/guide/cleanup.md +19 -0
- package/docs/guide/delegate.md +15 -3
- package/docs/guide/dispatch.md +2 -1
- package/docs/guide/getting-started.md +8 -3
- package/docs/reference/env.md +2 -2
- package/package.json +1 -1
- package/skills/mu/SKILL.md +2 -2
- package/skills/mu/recipes/orchestrator-loop.md +8 -2
- package/skills/mu/recipes/recovery.md +3 -0
- package/skills/mu/recipes/tasks-or-calls.md +9 -5
package/dist/extension/mu-pi.js
CHANGED
|
@@ -51,11 +51,18 @@ var DELEGATE_CANCEL_TOOL = "mu_delegate_cancel";
|
|
|
51
51
|
var DELEGATE_MESSAGE_TYPE = "mu-delegate";
|
|
52
52
|
var DELEGATE_WORKSTREAM = "scratch";
|
|
53
53
|
var DELEGATE_STATUS_KEY = "mu-delegate";
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
var PENDING_CHANNEL = "murmur:pending";
|
|
55
|
+
function delegateStatus(running, starting = 0, queued = 0, failed = 0) {
|
|
56
|
+
const parts = [];
|
|
57
|
+
if (running > 0) parts.push(`${running} delegate${running === 1 ? "" : "s"} running`);
|
|
58
|
+
if (starting > 0) parts.push(`${starting} starting`);
|
|
59
|
+
if (queued > 0) parts.push(`${queued} queued`);
|
|
60
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
61
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
56
62
|
}
|
|
57
63
|
var DELEGATE_TIMEOUT_S = 3600;
|
|
58
64
|
var DELEGATE_MAX_DEFAULT = 16;
|
|
65
|
+
var DELEGATE_QUEUE_FACTOR = 4;
|
|
59
66
|
function delegateMax(env = process.env) {
|
|
60
67
|
const n = Number(env.MU_DELEGATE_MAX);
|
|
61
68
|
return Number.isInteger(n) && n > 0 ? n : DELEGATE_MAX_DEFAULT;
|
|
@@ -106,7 +113,7 @@ function formatElapsed(ms) {
|
|
|
106
113
|
}
|
|
107
114
|
function delegateMessage(name, agent, pane, x = {}) {
|
|
108
115
|
const timeoutS = x.timeoutS ?? DELEGATE_TIMEOUT_S;
|
|
109
|
-
const who = `Delegate ${W}/${name}`;
|
|
116
|
+
const who = `Delegate ${W}/${name}${x.queuedAs ? ` (queued as ${x.queuedAs})` : ""}`;
|
|
110
117
|
const took = x.elapsedMs !== void 0 ? ` after ${formatElapsed(x.elapsedMs)}` : "";
|
|
111
118
|
const fate = pane.closed ? "Pane closed." : `Pane kept (${pane.why}).`;
|
|
112
119
|
const look = [
|
|
@@ -125,6 +132,13 @@ ${x.tail.trimEnd()}` : "";
|
|
|
125
132
|
${agent.lastText ?? ""}`;
|
|
126
133
|
case "empty":
|
|
127
134
|
return `${who} finished${took} without a text answer. ${fate}${ws}${tailBlock}`;
|
|
135
|
+
case "error":
|
|
136
|
+
return [
|
|
137
|
+
`${who} stopped on an API error${took}, after pi's own retries: ${(agent.lastError ?? "unknown error").replace(/[.\s]+$/, "")}. ${fate}${ws}`,
|
|
138
|
+
"Decide: re-issue the call, or record the check as UNVERIFIED.",
|
|
139
|
+
...look,
|
|
140
|
+
`Close: mu agent close ${name} -w ${W}`
|
|
141
|
+
].join("\n");
|
|
128
142
|
case "died":
|
|
129
143
|
return `${who} died${took} before answering. ${fate}${ws}${tailBlock}`;
|
|
130
144
|
case "timeout":
|
|
@@ -147,11 +161,28 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
147
161
|
if (!delegateEnabled()) return;
|
|
148
162
|
const inflight = /* @__PURE__ */ new Map();
|
|
149
163
|
let starting = 0;
|
|
164
|
+
const queue = [];
|
|
165
|
+
let queueSeq = 0;
|
|
166
|
+
const failed = /* @__PURE__ */ new Set();
|
|
150
167
|
const reserved = /* @__PURE__ */ new Set();
|
|
151
168
|
let ui;
|
|
169
|
+
let lastPending = 0;
|
|
170
|
+
const reportPending = () => {
|
|
171
|
+
const count = inflight.size + starting + queue.length;
|
|
172
|
+
if (count === lastPending) return;
|
|
173
|
+
lastPending = count;
|
|
174
|
+
try {
|
|
175
|
+
pi.events.emit(PENDING_CHANNEL, { source: DELEGATE_TOOL, count });
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
};
|
|
152
179
|
const showStatus = () => {
|
|
180
|
+
reportPending();
|
|
153
181
|
try {
|
|
154
|
-
ui?.setStatus?.(
|
|
182
|
+
ui?.setStatus?.(
|
|
183
|
+
DELEGATE_STATUS_KEY,
|
|
184
|
+
delegateStatus(inflight.size, starting, queue.length, failed.size)
|
|
185
|
+
);
|
|
155
186
|
} catch {
|
|
156
187
|
}
|
|
157
188
|
};
|
|
@@ -161,8 +192,34 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
161
192
|
const forget = (name) => {
|
|
162
193
|
inflight.delete(name);
|
|
163
194
|
reserved.delete(name);
|
|
195
|
+
drain();
|
|
164
196
|
showStatus();
|
|
165
197
|
};
|
|
198
|
+
function drain() {
|
|
199
|
+
while (queue.length > 0 && inflight.size + starting < delegateMax()) {
|
|
200
|
+
const q = queue.shift();
|
|
201
|
+
if (!q) break;
|
|
202
|
+
starting++;
|
|
203
|
+
void start(q.params, void 0, q.ctx, q.handle).then(
|
|
204
|
+
() => {
|
|
205
|
+
},
|
|
206
|
+
(e) => {
|
|
207
|
+
void Promise.resolve(
|
|
208
|
+
pi.sendMessage(
|
|
209
|
+
{
|
|
210
|
+
customType: DELEGATE_MESSAGE_TYPE,
|
|
211
|
+
content: `Queued delegate ${q.handle} could not start: ${e instanceof Error ? e.message : String(e)}`,
|
|
212
|
+
display: true,
|
|
213
|
+
details: { name: q.handle, workstream: W, outcome: null, closed: false }
|
|
214
|
+
},
|
|
215
|
+
{ deliverAs: "followUp", triggerTurn: true }
|
|
216
|
+
)
|
|
217
|
+
).catch(() => {
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
166
223
|
const mu = run;
|
|
167
224
|
const json = (r) => {
|
|
168
225
|
try {
|
|
@@ -179,6 +236,10 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
179
236
|
const taken = new Set(
|
|
180
237
|
Array.isArray(agents) ? agents.map((a) => a.name).filter((n) => n) : []
|
|
181
238
|
);
|
|
239
|
+
if (Array.isArray(agents)) {
|
|
240
|
+
for (const f of [...failed]) if (!taken.has(f)) failed.delete(f);
|
|
241
|
+
showStatus();
|
|
242
|
+
}
|
|
182
243
|
for (let n = 1; ; n++) {
|
|
183
244
|
const name = stem ? n === 1 ? `delegate-${stem}` : `delegate-${stem}-${n}` : `delegate-${n}`;
|
|
184
245
|
if (!taken.has(name) && !reserved.has(name)) {
|
|
@@ -230,7 +291,7 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
230
291
|
async function deliver(name, wait, keep, extras) {
|
|
231
292
|
const agent = json(wait)?.agents?.[0];
|
|
232
293
|
const outcome = agent?.outcome;
|
|
233
|
-
const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
|
|
294
|
+
const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" || outcome === "error" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
|
|
234
295
|
let pane = { closed: false, why: "keep: true" };
|
|
235
296
|
if (outcome !== "done" && outcome !== "empty")
|
|
236
297
|
pane = { closed: false, why: "kept as evidence" };
|
|
@@ -238,6 +299,10 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
238
299
|
const c = await mu(["agent", "close", name, "-w", W]);
|
|
239
300
|
pane = c.code === 0 ? { closed: true } : { closed: false, why: `close refused: ${(c.stderr || c.stdout).trim()}` };
|
|
240
301
|
}
|
|
302
|
+
if (outcome === "error" && !pane.closed) {
|
|
303
|
+
failed.add(name);
|
|
304
|
+
showStatus();
|
|
305
|
+
}
|
|
241
306
|
await pi.sendMessage(
|
|
242
307
|
{
|
|
243
308
|
customType: DELEGATE_MESSAGE_TYPE,
|
|
@@ -256,10 +321,106 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
256
321
|
{ deliverAs: "followUp", triggerTurn: true }
|
|
257
322
|
);
|
|
258
323
|
}
|
|
324
|
+
async function start(params, signal, ctx, queuedAs) {
|
|
325
|
+
let slotHeld = true;
|
|
326
|
+
const freeSlot = () => {
|
|
327
|
+
if (slotHeld) {
|
|
328
|
+
starting--;
|
|
329
|
+
showStatus();
|
|
330
|
+
}
|
|
331
|
+
slotHeld = false;
|
|
332
|
+
};
|
|
333
|
+
showStatus();
|
|
334
|
+
try {
|
|
335
|
+
const task = typeof params.task === "string" ? params.task.trim() : "";
|
|
336
|
+
const brief = typeof params.brief === "string" ? params.brief.trim() : "";
|
|
337
|
+
const keep = params.keep === true;
|
|
338
|
+
const t = params.timeout;
|
|
339
|
+
if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
|
|
340
|
+
throw new Error(
|
|
341
|
+
`mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
|
|
342
|
+
);
|
|
343
|
+
const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
|
|
344
|
+
let cwd;
|
|
345
|
+
if (params.workspace !== true) {
|
|
346
|
+
cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
|
|
347
|
+
if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
|
|
348
|
+
}
|
|
349
|
+
const { name, attach, workspace } = await spawn(params, cwd, signal);
|
|
350
|
+
const entry = { abort: new AbortController(), cancelling: false };
|
|
351
|
+
inflight.set(name, entry);
|
|
352
|
+
freeSlot();
|
|
353
|
+
showStatus();
|
|
354
|
+
const started = Date.now();
|
|
355
|
+
const waiting = mu(
|
|
356
|
+
["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
|
|
357
|
+
entry.abort.signal
|
|
358
|
+
);
|
|
359
|
+
const sent = await mu(["agent", "send", name, brief ? `${brief}
|
|
360
|
+
|
|
361
|
+
${task}` : task, "-w", W]);
|
|
362
|
+
if (sent.code !== 0) {
|
|
363
|
+
entry.abort.abort();
|
|
364
|
+
forget(name);
|
|
365
|
+
const c = await mu(["agent", "close", name, "-w", W]);
|
|
366
|
+
const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
|
|
367
|
+
throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
|
|
368
|
+
}
|
|
369
|
+
const settle = async (r) => {
|
|
370
|
+
forget(name);
|
|
371
|
+
try {
|
|
372
|
+
await deliver(name, r, keep, {
|
|
373
|
+
timeoutS,
|
|
374
|
+
elapsedMs: Date.now() - started,
|
|
375
|
+
...queuedAs ? { queuedAs } : {},
|
|
376
|
+
...attach ? { attach } : {},
|
|
377
|
+
...workspace ? { workspace } : {}
|
|
378
|
+
});
|
|
379
|
+
} catch (e) {
|
|
380
|
+
await Promise.resolve(
|
|
381
|
+
pi.sendMessage(
|
|
382
|
+
{
|
|
383
|
+
customType: DELEGATE_MESSAGE_TYPE,
|
|
384
|
+
content: `Delegate ${W}/${name} settled, but delivering its answer failed: ${e instanceof Error ? e.message : String(e)}. Read it with: mu agent read ${name} -n 50 -w ${W}`,
|
|
385
|
+
display: true,
|
|
386
|
+
details: { name, workstream: W, outcome: null, closed: false }
|
|
387
|
+
},
|
|
388
|
+
{ deliverAs: "followUp", triggerTurn: true }
|
|
389
|
+
)
|
|
390
|
+
).catch(() => {
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
void waiting.then(async (r) => {
|
|
395
|
+
if (inflight.get(name) !== entry) return;
|
|
396
|
+
if (entry.cancelling) entry.parked = () => settle(r);
|
|
397
|
+
else await settle(r);
|
|
398
|
+
});
|
|
399
|
+
const lines = [
|
|
400
|
+
`Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
|
|
401
|
+
workspace ? `Workspace: ${workspace}` : void 0,
|
|
402
|
+
attach ? `Attach: ${attach}` : void 0,
|
|
403
|
+
`Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
|
|
404
|
+
].filter((x) => x !== void 0);
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
407
|
+
details: {
|
|
408
|
+
name,
|
|
409
|
+
workstream: W,
|
|
410
|
+
attach: attach ?? null,
|
|
411
|
+
workspace: workspace ?? null,
|
|
412
|
+
keep
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
} finally {
|
|
416
|
+
freeSlot();
|
|
417
|
+
if (queuedAs) drain();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
259
420
|
pi.registerTool({
|
|
260
421
|
name: DELEGATE_TOOL,
|
|
261
422
|
label: "mu delegate",
|
|
262
|
-
description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation; call it several times to fan out in parallel (at most ${delegateMax()}
|
|
423
|
+
description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation; call it several times to fan out in parallel (at most ${delegateMax()} run at once; up to ${DELEGATE_QUEUE_FACTOR * delegateMax()} more are queued and start as slots free). The subagent starts with no context, in your working directory: put everything it needs in task. The call returns at once and the answer arrives later as a follow-up message: carry on with other work, or end your turn if your next step needs the answer (the follow-up resumes you). The pane closes after a clean finish; keep: true leaves it open.`,
|
|
263
424
|
promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
|
|
264
425
|
parameters: {
|
|
265
426
|
type: "object",
|
|
@@ -302,106 +463,43 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
302
463
|
track(ctx);
|
|
303
464
|
const task = typeof params.task === "string" ? params.task.trim() : "";
|
|
304
465
|
if (!task) throw new Error("mu_delegate needs a non-empty task");
|
|
305
|
-
const
|
|
306
|
-
if (
|
|
466
|
+
const t = params.timeout;
|
|
467
|
+
if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
|
|
307
468
|
throw new Error(
|
|
308
|
-
`mu_delegate:
|
|
469
|
+
`mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
|
|
309
470
|
);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
`mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
|
|
323
|
-
);
|
|
324
|
-
const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
|
|
325
|
-
let cwd;
|
|
326
|
-
if (params.workspace !== true) {
|
|
327
|
-
cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
|
|
328
|
-
if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
|
|
329
|
-
}
|
|
330
|
-
const { name, attach, workspace } = await spawn(params, cwd, signal);
|
|
331
|
-
const entry = { abort: new AbortController(), cancelling: false };
|
|
332
|
-
inflight.set(name, entry);
|
|
333
|
-
freeSlot();
|
|
334
|
-
showStatus();
|
|
335
|
-
const started = Date.now();
|
|
336
|
-
const waiting = mu(
|
|
337
|
-
["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
|
|
338
|
-
entry.abort.signal
|
|
471
|
+
if (params.workspace !== true) {
|
|
472
|
+
const cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
|
|
473
|
+
if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
|
|
474
|
+
}
|
|
475
|
+
const max = delegateMax();
|
|
476
|
+
if (inflight.size + starting < max) {
|
|
477
|
+
starting++;
|
|
478
|
+
return start(params, signal, ctx);
|
|
479
|
+
}
|
|
480
|
+
if (queue.length >= DELEGATE_QUEUE_FACTOR * max)
|
|
481
|
+
throw new Error(
|
|
482
|
+
`mu_delegate: ${inflight.size + starting} running and ${queue.length} queued (MU_DELEGATE_MAX=${max}). End your turn; issue the rest as answers arrive (each answer resumes you).`
|
|
339
483
|
);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
|
|
356
|
-
}
|
|
357
|
-
const settle = async (r) => {
|
|
358
|
-
forget(name);
|
|
359
|
-
try {
|
|
360
|
-
await deliver(name, r, keep, {
|
|
361
|
-
timeoutS,
|
|
362
|
-
elapsedMs: Date.now() - started,
|
|
363
|
-
...attach ? { attach } : {},
|
|
364
|
-
...workspace ? { workspace } : {}
|
|
365
|
-
});
|
|
366
|
-
} catch (e) {
|
|
367
|
-
await Promise.resolve(
|
|
368
|
-
pi.sendMessage(
|
|
369
|
-
{
|
|
370
|
-
customType: DELEGATE_MESSAGE_TYPE,
|
|
371
|
-
content: `Delegate ${W}/${name} settled, but delivering its answer failed: ${e instanceof Error ? e.message : String(e)}. Read it with: mu agent read ${name} -n 50 -w ${W}`,
|
|
372
|
-
display: true,
|
|
373
|
-
details: { name, workstream: W, outcome: null, closed: false }
|
|
374
|
-
},
|
|
375
|
-
{ deliverAs: "followUp", triggerTurn: true }
|
|
376
|
-
)
|
|
377
|
-
).catch(() => {
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
void waiting.then(async (r) => {
|
|
382
|
-
if (inflight.get(name) !== entry) return;
|
|
383
|
-
if (entry.cancelling) entry.parked = () => settle(r);
|
|
384
|
-
else await settle(r);
|
|
385
|
-
});
|
|
386
|
-
const lines = [
|
|
387
|
-
`Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
|
|
388
|
-
workspace ? `Workspace: ${workspace}` : void 0,
|
|
389
|
-
attach ? `Attach: ${attach}` : void 0,
|
|
390
|
-
`Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
|
|
391
|
-
].filter((x) => x !== void 0);
|
|
392
|
-
return {
|
|
393
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
394
|
-
details: {
|
|
395
|
-
name,
|
|
396
|
-
workstream: W,
|
|
397
|
-
attach: attach ?? null,
|
|
398
|
-
workspace: workspace ?? null,
|
|
399
|
-
keep
|
|
484
|
+
const handle = `queued-${++queueSeq}`;
|
|
485
|
+
queue.push({
|
|
486
|
+
handle,
|
|
487
|
+
params: {
|
|
488
|
+
...params,
|
|
489
|
+
cwd: typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd()
|
|
490
|
+
},
|
|
491
|
+
ctx
|
|
492
|
+
});
|
|
493
|
+
showStatus();
|
|
494
|
+
return {
|
|
495
|
+
content: [
|
|
496
|
+
{
|
|
497
|
+
type: "text",
|
|
498
|
+
text: `Queued as ${handle} (#${queue.length}; ${inflight.size + starting} running, MU_DELEGATE_MAX=${max}). It starts when a slot frees, and its answer arrives as a follow-up message like any delegate's; do not wait or poll. Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${handle}" }`
|
|
400
499
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
500
|
+
],
|
|
501
|
+
details: { name: handle, workstream: W, queued: true, position: queue.length }
|
|
502
|
+
};
|
|
405
503
|
}
|
|
406
504
|
});
|
|
407
505
|
pi.registerTool({
|
|
@@ -421,6 +519,15 @@ ${task}` : task,
|
|
|
421
519
|
track(ctx);
|
|
422
520
|
const name = typeof params.name === "string" ? params.name : "";
|
|
423
521
|
if (!name) throw new Error("mu_delegate_cancel needs a name");
|
|
522
|
+
const qi = queue.findIndex((q) => q.handle === name);
|
|
523
|
+
if (qi >= 0) {
|
|
524
|
+
queue.splice(qi, 1);
|
|
525
|
+
showStatus();
|
|
526
|
+
return {
|
|
527
|
+
content: [{ type: "text", text: `Dropped ${name} from the queue; it never started.` }],
|
|
528
|
+
details: { name, workstream: W }
|
|
529
|
+
};
|
|
530
|
+
}
|
|
424
531
|
const entry = inflight.get(name);
|
|
425
532
|
if (entry) entry.cancelling = true;
|
|
426
533
|
const ab = await mu(["agent", "abort", name, "-w", W]);
|
|
@@ -437,6 +544,7 @@ ${task}` : task,
|
|
|
437
544
|
entry.abort.abort();
|
|
438
545
|
forget(name);
|
|
439
546
|
}
|
|
547
|
+
failed.delete(name);
|
|
440
548
|
let text = `Aborted ${W}/${name}.`;
|
|
441
549
|
if (params.keep === true) text += " Pane kept.";
|
|
442
550
|
else {
|
|
@@ -447,13 +555,21 @@ ${task}` : task,
|
|
|
447
555
|
}
|
|
448
556
|
});
|
|
449
557
|
pi.on("session_shutdown", (_e, ctx) => {
|
|
450
|
-
if (inflight.size === 0) return;
|
|
558
|
+
if (inflight.size === 0 && queue.length === 0) return;
|
|
451
559
|
const names = [...inflight.keys()];
|
|
560
|
+
const never = queue.splice(0).map((q) => q.handle);
|
|
452
561
|
for (const e of inflight.values()) e.abort.abort();
|
|
453
562
|
inflight.clear();
|
|
454
563
|
showStatus();
|
|
455
564
|
ui = void 0;
|
|
456
|
-
const
|
|
565
|
+
const parts = [];
|
|
566
|
+
if (names.length > 0)
|
|
567
|
+
parts.push(
|
|
568
|
+
`${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`
|
|
569
|
+
);
|
|
570
|
+
if (never.length > 0)
|
|
571
|
+
parts.push(`${never.length} queued, never started: ${never.join(", ")}. Re-issue them.`);
|
|
572
|
+
const msg = parts.join(" ");
|
|
457
573
|
if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
|
|
458
574
|
else process.stderr.write(`mu: ${msg}
|
|
459
575
|
`);
|
|
@@ -465,6 +581,7 @@ import { existsSync as existsSync2, readFileSync } from "fs";
|
|
|
465
581
|
import { join as join2 } from "path";
|
|
466
582
|
var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
|
|
467
583
|
var NUDGE_LOG_KIND = "nudge";
|
|
584
|
+
var CLOSE_NUDGE_MESSAGE_TYPE = "mu-close-task";
|
|
468
585
|
var MARK_START = "<!-- mu:keep-driving -->";
|
|
469
586
|
var MARK_END = "<!-- /mu:keep-driving -->";
|
|
470
587
|
var WAIT_HINT_MAX = 8;
|
|
@@ -588,9 +705,71 @@ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
|
|
|
588
705
|
};
|
|
589
706
|
});
|
|
590
707
|
}
|
|
708
|
+
function workerIdentity(env = process.env) {
|
|
709
|
+
const agent = env.MU_AGENT_NAME;
|
|
710
|
+
const workstream = env.MU_WORKSTREAM;
|
|
711
|
+
if (!agent || !workstream || workstream === "scratch") return void 0;
|
|
712
|
+
return { agent, workstream };
|
|
713
|
+
}
|
|
714
|
+
async function ownedInProgress(run, agent, workstream) {
|
|
715
|
+
const r = await run(["task", "owned-by", agent, "-w", workstream, "--json"]);
|
|
716
|
+
if (r.code !== 0) return void 0;
|
|
717
|
+
try {
|
|
718
|
+
const json = JSON.parse(r.stdout);
|
|
719
|
+
return (json.items ?? []).flatMap(
|
|
720
|
+
(t) => t.status === "IN_PROGRESS" && t.name ? [t.name] : []
|
|
721
|
+
);
|
|
722
|
+
} catch {
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function closeNudgeText(workstream, names) {
|
|
727
|
+
const close = names.map((n) => `mu task close ${n} -w ${workstream} --evidence "<what you verified>"`).join("\n ");
|
|
728
|
+
return [
|
|
729
|
+
`[mu] You are stopping while you still own ${names.map((n) => `${workstream}/${n}`).join(", ")} (IN_PROGRESS).`,
|
|
730
|
+
"If the work is done and verified, close it now:",
|
|
731
|
+
` ${close}`,
|
|
732
|
+
"If you are blocked or need an answer, say so in one line and stop; the orchestrator will read your pane."
|
|
733
|
+
].join("\n");
|
|
734
|
+
}
|
|
735
|
+
function registerCloseNudge(pi, run = defaultRunner(), env = process.env) {
|
|
736
|
+
if (!nudgeEnabled(env)) return;
|
|
737
|
+
const me = workerIdentity(env);
|
|
738
|
+
if (me === void 0) return;
|
|
739
|
+
let fired = false;
|
|
740
|
+
pi.on("input", () => {
|
|
741
|
+
fired = false;
|
|
742
|
+
});
|
|
743
|
+
pi.on("agent_before_settle", async (event) => {
|
|
744
|
+
if (fired) return;
|
|
745
|
+
if (outcomeOf(event) !== "completed") return;
|
|
746
|
+
fired = true;
|
|
747
|
+
const names = await ownedInProgress(run, me.agent, me.workstream);
|
|
748
|
+
if (names === void 0 || names.length === 0) return;
|
|
749
|
+
await run([
|
|
750
|
+
"log",
|
|
751
|
+
"-w",
|
|
752
|
+
me.workstream,
|
|
753
|
+
"--kind",
|
|
754
|
+
NUDGE_LOG_KIND,
|
|
755
|
+
`close: ${me.agent} settled owning ${names.join(", ")}`
|
|
756
|
+
]);
|
|
757
|
+
return {
|
|
758
|
+
entries: [
|
|
759
|
+
{
|
|
760
|
+
type: "custom_message",
|
|
761
|
+
customType: CLOSE_NUDGE_MESSAGE_TYPE,
|
|
762
|
+
content: closeNudgeText(me.workstream, names),
|
|
763
|
+
display: true
|
|
764
|
+
}
|
|
765
|
+
],
|
|
766
|
+
continue: true
|
|
767
|
+
};
|
|
768
|
+
});
|
|
769
|
+
}
|
|
591
770
|
|
|
592
771
|
// extension/mu-pi.ts
|
|
593
|
-
var EXT_VERSION = true ? "3.
|
|
772
|
+
var EXT_VERSION = true ? "3.6.0" : void 0;
|
|
594
773
|
var FRESH_COMMAND = "mu-fresh";
|
|
595
774
|
var FRESH_TIMEOUT_MS = 3e4;
|
|
596
775
|
function commandName(name) {
|
|
@@ -661,6 +840,19 @@ function lastAssistantText(event) {
|
|
|
661
840
|
}
|
|
662
841
|
return "";
|
|
663
842
|
}
|
|
843
|
+
function lastAssistantError(event) {
|
|
844
|
+
const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
|
|
845
|
+
if (!Array.isArray(msgs)) return void 0;
|
|
846
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
847
|
+
const m = msgs[i];
|
|
848
|
+
if (typeof m !== "object" || m === null) continue;
|
|
849
|
+
const { role, stopReason, errorMessage } = m;
|
|
850
|
+
if (role !== "assistant") continue;
|
|
851
|
+
if (stopReason !== "error") return void 0;
|
|
852
|
+
return typeof errorMessage === "string" && errorMessage !== "" ? capText(errorMessage) : "unknown error";
|
|
853
|
+
}
|
|
854
|
+
return void 0;
|
|
855
|
+
}
|
|
664
856
|
function capText(text) {
|
|
665
857
|
if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return text;
|
|
666
858
|
const cut = Buffer.from(text, "utf8").subarray(0, LAST_TEXT_MAX_BYTES).toString("utf8");
|
|
@@ -720,6 +912,7 @@ function muPi(pi) {
|
|
|
720
912
|
registerDelegate(pi);
|
|
721
913
|
registerPrompts(pi);
|
|
722
914
|
registerNudge(pi);
|
|
915
|
+
registerCloseNudge(pi);
|
|
723
916
|
serveCtl(pi);
|
|
724
917
|
}
|
|
725
918
|
function serveCtl(pi) {
|
|
@@ -768,6 +961,7 @@ function serveCtl(pi) {
|
|
|
768
961
|
});
|
|
769
962
|
pi.on("agent_end", (e) => {
|
|
770
963
|
g.endText = lastAssistantText(e);
|
|
964
|
+
g.endError = lastAssistantError(e);
|
|
771
965
|
});
|
|
772
966
|
pi.on("agent_settled", (_e, c) => {
|
|
773
967
|
g.ctx = c;
|
|
@@ -776,6 +970,8 @@ function serveCtl(pi) {
|
|
|
776
970
|
g.runs++;
|
|
777
971
|
g.lastText = g.endText ?? "";
|
|
778
972
|
g.endText = void 0;
|
|
973
|
+
g.lastError = g.endError;
|
|
974
|
+
g.endError = void 0;
|
|
779
975
|
for (const w of [...g.waiters]) w();
|
|
780
976
|
});
|
|
781
977
|
pi.registerCommand(FRESH_COMMAND, {
|
|
@@ -914,9 +1110,13 @@ function serveCtl(pi) {
|
|
|
914
1110
|
return { ...fail(`${UNKNOWN_OP_PREFIX}${String(req.op)}`), ops: [...CTL_OPS] };
|
|
915
1111
|
}
|
|
916
1112
|
}
|
|
1113
|
+
const settledRun = () => ({
|
|
1114
|
+
lastText: g.lastText,
|
|
1115
|
+
...g.lastError !== void 0 ? { lastError: g.lastError } : {}
|
|
1116
|
+
});
|
|
917
1117
|
function wait(afterRuns, timeoutMs, conn) {
|
|
918
1118
|
if (afterRuns !== void 0 && g.runs > afterRuns && g.state === "idle") {
|
|
919
|
-
return Promise.resolve({ v: V, ok: true, ...status(),
|
|
1119
|
+
return Promise.resolve({ v: V, ok: true, ...status(), ...settledRun() });
|
|
920
1120
|
}
|
|
921
1121
|
return new Promise((resolve) => {
|
|
922
1122
|
let timer;
|
|
@@ -926,7 +1126,7 @@ function serveCtl(pi) {
|
|
|
926
1126
|
conn.off("close", onClose);
|
|
927
1127
|
resolve(r);
|
|
928
1128
|
};
|
|
929
|
-
const waiter = () => done({ v: V, ok: true, ...status(),
|
|
1129
|
+
const waiter = () => done({ v: V, ok: true, ...status(), ...settledRun() });
|
|
930
1130
|
const onClose = () => done(fail("client closed"));
|
|
931
1131
|
g.waiters.add(waiter);
|
|
932
1132
|
conn.on("close", onClose);
|
|
@@ -1041,6 +1241,7 @@ export {
|
|
|
1041
1241
|
LIVE_PROBE_MS,
|
|
1042
1242
|
commandName,
|
|
1043
1243
|
muPi as default,
|
|
1244
|
+
lastAssistantError,
|
|
1044
1245
|
lastAssistantText,
|
|
1045
1246
|
promptsDir
|
|
1046
1247
|
};
|