@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.
- package/AGENTS.md +73 -35
- package/README.md +71 -57
- package/dist/chunk-4ULY3ZKO.js +4482 -0
- package/dist/chunk-4ULY3ZKO.js.map +1 -0
- package/dist/chunk-7M634JMD.js +375 -0
- package/dist/chunk-7M634JMD.js.map +1 -0
- package/dist/{chunk-3BARPJLY.js → chunk-PZ5GOCWH.js} +16 -2
- package/dist/chunk-PZ5GOCWH.js.map +1 -0
- package/dist/chunk-SS7NQSSZ.js +3660 -0
- package/dist/chunk-SS7NQSSZ.js.map +1 -0
- package/dist/chunk-UCZPJX7K.js +14253 -0
- package/dist/chunk-UCZPJX7K.js.map +1 -0
- package/dist/{cli-CKOMHBXV.js → cli-TV3N47AZ.js} +7317 -4593
- package/dist/cli-TV3N47AZ.js.map +1 -0
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/errors-GRONKWFS.js +65 -0
- package/dist/extension/mu-pi.js +1632 -837
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +4067 -2561
- package/dist/index.js +614 -297
- package/dist/index.js.map +1 -1
- package/dist/tui-FUBQ326D.js +1848 -0
- package/dist/tui-FUBQ326D.js.map +1 -0
- package/docs/ARCHITECTURE.md +118 -66
- package/docs/VOCABULARY.md +113 -49
- package/docs/guide/README.md +3 -3
- package/docs/guide/backends.md +8 -4
- package/docs/guide/cleanup.md +69 -33
- package/docs/guide/delegate.md +149 -66
- package/docs/guide/dispatch.md +148 -53
- package/docs/guide/getting-started.md +37 -44
- package/docs/guide/other-agent-clis.md +102 -0
- package/docs/guide/recovery.md +31 -50
- package/docs/guide/remote.md +122 -45
- package/docs/guide/sql.md +104 -24
- package/docs/guide/stop-a-worker.md +30 -37
- package/docs/guide/sync.md +11 -3
- package/docs/guide/upgrade.md +93 -43
- package/docs/reference/env.md +24 -32
- package/docs/reference/naming.md +14 -7
- package/docs/reference/surface-contract.md +153 -0
- package/package.json +33 -11
- package/prompts/mu-debug.md +3 -1
- package/prompts/mu-refute.md +2 -1
- package/prompts/mu-research.md +3 -1
- package/prompts/mu-review.md +2 -1
- package/prompts/mu-rules-audit.md +5 -4
- package/prompts/mu-sweep.md +2 -1
- package/prompts/mu-tournament.md +4 -2
- package/prompts/mu-until.md +3 -1
- package/prompts/ultrathink.md +1 -1
- package/skills/mu/SKILL.md +60 -291
- package/skills/mu/recipes/adversarial-review.md +12 -103
- package/skills/mu/recipes/brief.md +14 -61
- package/skills/mu/recipes/course-correct.md +23 -0
- package/skills/mu/recipes/findings.md +17 -111
- package/skills/mu/recipes/models.md +17 -92
- package/skills/mu/recipes/move.md +14 -0
- package/skills/mu/recipes/orchestrator-loop.md +20 -119
- package/skills/mu/recipes/recovery.md +8 -29
- package/skills/mu/recipes/remote-workers.md +17 -318
- package/skills/mu/recipes/tasks-or-calls.md +15 -78
- package/skills/mu/recipes/test-health.md +19 -77
- package/skills/mu/recipes/worker.md +17 -24
- package/dist/chunk-45KQLMEL.js +0 -2908
- package/dist/chunk-45KQLMEL.js.map +0 -1
- package/dist/chunk-G434JKV2.js +0 -641
- package/dist/chunk-G434JKV2.js.map +0 -1
- package/dist/chunk-KUGUFBWH.js +0 -12527
- package/dist/chunk-KUGUFBWH.js.map +0 -1
- package/dist/chunk-QITLNZEM.js +0 -87
- package/dist/chunk-QITLNZEM.js.map +0 -1
- package/dist/chunk-VIEYQMQA.js +0 -393
- package/dist/chunk-VIEYQMQA.js.map +0 -1
- package/dist/chunk-WI6AWCMK.js +0 -19
- package/dist/chunk-WI6AWCMK.js.map +0 -1
- package/dist/cli-CKOMHBXV.js.map +0 -1
- package/dist/devtools-LS4ZEPWM.js +0 -3772
- package/dist/devtools-LS4ZEPWM.js.map +0 -1
- package/dist/tui-LFJ5P4Y2.js +0 -40956
- package/dist/tui-LFJ5P4Y2.js.map +0 -1
- package/dist/tui-sync-worker.js +0 -17
- package/dist/tui-sync-worker.js.map +0 -1
- package/dist/yank-XDOONQSL.js +0 -13
- package/dist/yank-XDOONQSL.js.map +0 -1
- package/docs/ROADMAP.md +0 -123
- package/docs/VISION.md +0 -298
- package/docs/architecture/control-socket.md +0 -142
- package/docs/architecture/dag.md +0 -117
- package/docs/architecture/mux.md +0 -167
- package/docs/architecture/ops-log.md +0 -160
- package/docs/architecture/sdk.md +0 -187
- package/docs/architecture/sync.md +0 -159
- package/docs/architecture/tui.md +0 -165
- package/docs/guide/tui.md +0 -98
- package/docs/history/CHANGELOG-pre-3.md +0 -4866
- package/docs/img/tui-dashboard.png +0 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +0 -333
- package/docs/specs/2026-09-29-task-substates.md +0 -247
- package/docs/specs/2026-10-03-keep-driving-nudge.md +0 -163
- package/docs/specs/2026-10-21-fast-and-slow-gates.md +0 -229
- package/skills/mu/recipes/backlog-triage.md +0 -45
- package/skills/mu/recipes/codemode-driver.md +0 -118
- package/skills/mu/recipes/deep-research.md +0 -69
- package/skills/mu/recipes/drift-audit.md +0 -75
- package/skills/mu/recipes/fan-out.md +0 -42
- package/skills/mu/recipes/hypothesis-panel.md +0 -48
- package/skills/mu/recipes/long-run.md +0 -32
- package/skills/mu/recipes/loop-until-done.md +0 -45
- package/skills/mu/recipes/plan.md +0 -67
- package/skills/mu/recipes/refute.md +0 -55
- package/skills/mu/recipes/review-panel.md +0 -65
- package/skills/mu/recipes/rules-audit.md +0 -89
- package/skills/mu/recipes/tournament.md +0 -44
- package/skills/mu/recipes/ultrathink.md +0 -86
- package/skills/mu/recipes/watcher.md +0 -28
- package/skills/mu/recipes/waves.md +0 -21
- /package/dist/{chunk-3BARPJLY.js.map → errors-GRONKWFS.js.map} +0 -0
package/dist/extension/mu-pi.js
CHANGED
|
@@ -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 {
|
|
47
|
-
import {
|
|
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/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
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
|
-
|
|
98
|
-
|
|
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) =>
|
|
108
|
-
|
|
109
|
-
|
|
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, ...
|
|
132
|
+
[...argv],
|
|
133
|
+
{ maxBuffer: 16 * 1024 * 1024, ...env ? { env } : {} },
|
|
113
134
|
(err, stdout, stderr) => {
|
|
114
|
-
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
201
|
-
`Close: mu agent close ${name} -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 ${
|
|
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
|
|
218
|
-
|
|
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
|
-
`
|
|
236
|
-
...
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
return
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
287
|
-
let lastPruneAt = 0;
|
|
562
|
+
const owed = /* @__PURE__ */ new Map();
|
|
288
563
|
const arriving = /* @__PURE__ */ new Map();
|
|
289
|
-
let
|
|
290
|
-
let
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
336
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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(
|
|
389
|
-
signal?.
|
|
390
|
-
|
|
391
|
-
const
|
|
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 = [
|
|
394
|
-
|
|
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 =
|
|
710
|
+
const cli = req.cli ? req.cli : void 0;
|
|
397
711
|
if (cli) args.push("--cli", cli);
|
|
398
|
-
if (
|
|
399
|
-
|
|
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"
|
|
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
|
|
407
|
-
|
|
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
|
|
411
|
-
`${
|
|
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
|
|
745
|
+
const sent = out?.send;
|
|
415
746
|
if (sent?.ok !== true && sent?.error === "AgentSendIndeterminateError") {
|
|
416
|
-
const
|
|
417
|
-
throw new
|
|
418
|
-
`mu agent send to ${
|
|
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
|
|
423
|
-
const pane = c.
|
|
424
|
-
const
|
|
425
|
-
throw new
|
|
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
|
|
428
|
-
const
|
|
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
|
|
445
|
-
if (
|
|
446
|
-
const
|
|
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?.
|
|
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.
|
|
816
|
+
return { workstream: t.workstream, task: t.id };
|
|
457
817
|
}
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
if (outcome
|
|
475
|
-
else if (
|
|
476
|
-
pane = { closed: false, why: "
|
|
477
|
-
else if (!keep)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
${
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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(
|
|
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
|
-
|
|
971
|
+
reportPending();
|
|
538
972
|
}
|
|
539
973
|
slotHeld = false;
|
|
540
974
|
};
|
|
541
|
-
|
|
975
|
+
reportPending();
|
|
976
|
+
const ws = call2.workstream;
|
|
977
|
+
const req = call2.request;
|
|
542
978
|
try {
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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 } =
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
...
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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 ${
|
|
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:
|
|
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
|
|
664
|
-
promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later
|
|
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;
|
|
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"
|
|
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
|
|
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
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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 (
|
|
730
|
-
|
|
731
|
-
|
|
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:
|
|
1266
|
+
`mu_delegate: could not register the call: ${errText(e)}. Nothing spawned.`
|
|
734
1267
|
);
|
|
735
|
-
|
|
1268
|
+
}
|
|
1269
|
+
return queuedResult(reg.call, "refused", handle, 0);
|
|
736
1270
|
}
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
750
|
-
|
|
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).
|
|
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: {
|
|
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
|
|
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
|
|
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
|
-
|
|
790
|
-
|
|
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
|
-
|
|
799
|
-
|
|
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 (
|
|
803
|
-
|
|
804
|
-
const parked =
|
|
805
|
-
|
|
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 ${
|
|
1427
|
+
throw failure(`mu agent abort ${ws}/${agentName}`, ab);
|
|
809
1428
|
}
|
|
810
|
-
if (
|
|
811
|
-
|
|
812
|
-
forget(
|
|
1429
|
+
if (entry2 && key) {
|
|
1430
|
+
entry2.abort.abort();
|
|
1431
|
+
forget(key);
|
|
813
1432
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
|
827
|
-
text += c.
|
|
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:
|
|
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?.
|
|
838
|
-
if (typeof id
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
if (!a
|
|
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
|
-
|
|
847
|
-
|
|
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
|
-
|
|
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("
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
|
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
|
-
([
|
|
1570
|
+
([k, e2]) => e2.sessionFile ? [`${shown(k)}: ${e2.sessionFile}`] : []
|
|
878
1571
|
);
|
|
879
|
-
const
|
|
880
|
-
|
|
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
|
-
|
|
883
|
-
|
|
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 (
|
|
891
|
-
parts.push(
|
|
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 {
|
|
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?.() ? "
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (
|
|
941
|
-
return
|
|
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 (
|
|
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
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
pi.on("input",
|
|
1191
|
-
|
|
1192
|
-
|
|
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 (
|
|
1857
|
+
if (nested() || hold(ctx)) return;
|
|
1201
1858
|
if (outcomeOf(event) !== "completed") return;
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
"
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
|
1240
|
-
|
|
1241
|
-
|
|
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 ? "
|
|
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
|
|
1382
|
-
if (!
|
|
1383
|
-
let dir = dirname(realpathSync(
|
|
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
|
|
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
|
|
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() ? "
|
|
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.
|
|
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
|
-
|
|
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
|
|
1930
|
-
if (
|
|
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
|