@mu-crew/mu 3.13.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +73 -35
- package/README.md +71 -57
- package/dist/chunk-5HECDBK3.js +4482 -0
- package/dist/chunk-5HECDBK3.js.map +1 -0
- package/dist/chunk-AW2NONYU.js +301 -0
- package/dist/chunk-AW2NONYU.js.map +1 -0
- package/dist/chunk-GQNHIPJN.js +4060 -0
- package/dist/chunk-GQNHIPJN.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-S424D5EP.js +13766 -0
- package/dist/chunk-S424D5EP.js.map +1 -0
- package/dist/{cli-CKOMHBXV.js → cli-GB2CAVVW.js} +6776 -4350
- package/dist/cli-GB2CAVVW.js.map +1 -0
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/errors-KHOYKD7D.js +65 -0
- package/dist/extension/mu-pi.js +1617 -836
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +4015 -2561
- package/dist/index.js +614 -297
- package/dist/index.js.map +1 -1
- package/dist/tui-ALPHYTA2.js +1773 -0
- package/dist/tui-ALPHYTA2.js.map +1 -0
- package/docs/ARCHITECTURE.md +117 -66
- package/docs/VOCABULARY.md +112 -49
- package/docs/guide/README.md +3 -3
- package/docs/guide/backends.md +8 -4
- package/docs/guide/cleanup.md +61 -33
- package/docs/guide/delegate.md +141 -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 +115 -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 +23 -32
- package/docs/reference/naming.md +12 -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 +16 -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-KHOYKD7D.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,447 @@ 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)}` };
|
|
387
679
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
680
|
+
class SpawnFail extends Error {
|
|
681
|
+
constructor(message, kind) {
|
|
682
|
+
super(message);
|
|
683
|
+
this.kind = kind;
|
|
684
|
+
}
|
|
685
|
+
kind;
|
|
686
|
+
}
|
|
687
|
+
async function spawn(call2, text, cwd, signal) {
|
|
688
|
+
if (signal?.aborted) throw new SpawnFail("cancelled before it spawned", "aborted");
|
|
689
|
+
const ws = call2.workstream;
|
|
690
|
+
const req = call2.request;
|
|
691
|
+
const stem = labelStem(req.label);
|
|
392
692
|
const base = stem ? `delegate-${stem}` : "delegate-1";
|
|
393
|
-
const args = [
|
|
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 = Date.now();
|
|
907
|
+
const waiting = mu(
|
|
908
|
+
[
|
|
909
|
+
"agent",
|
|
910
|
+
"wait",
|
|
911
|
+
name,
|
|
912
|
+
"-w",
|
|
913
|
+
ws,
|
|
914
|
+
"--json",
|
|
915
|
+
"--timeout",
|
|
916
|
+
String(o.timeoutS),
|
|
917
|
+
"--after-runs",
|
|
918
|
+
String(runs)
|
|
919
|
+
],
|
|
920
|
+
entry2.abort.signal
|
|
530
921
|
);
|
|
922
|
+
const settle = async (r) => {
|
|
923
|
+
trace("settle", { call: call2.callId, name, code: r.code });
|
|
924
|
+
forget(key);
|
|
925
|
+
try {
|
|
926
|
+
await deliver(
|
|
927
|
+
call2,
|
|
928
|
+
name,
|
|
929
|
+
r,
|
|
930
|
+
o.keep,
|
|
931
|
+
{
|
|
932
|
+
timeoutS: o.timeoutS,
|
|
933
|
+
elapsedMs: Date.now() - started,
|
|
934
|
+
...o.queuedAs ? { queuedAs: o.queuedAs } : {},
|
|
935
|
+
...o.attach ? { attach: o.attach } : {},
|
|
936
|
+
...o.workspace ? { workspace: o.workspace } : {},
|
|
937
|
+
...entry2.sessionFile ? { sessionFile: entry2.sessionFile } : {}
|
|
938
|
+
},
|
|
939
|
+
entry2.identity
|
|
940
|
+
);
|
|
941
|
+
} catch (e) {
|
|
942
|
+
const content = `Delegate ${ws}/${name} settled, but delivering its answer failed: ${e instanceof Error ? e.message : String(e)}. Read it with: mu agent read ${name} -n 50 -w ${ws}`;
|
|
943
|
+
await Promise.resolve(
|
|
944
|
+
send(
|
|
945
|
+
{
|
|
946
|
+
customType: DELEGATE_MESSAGE_TYPE,
|
|
947
|
+
content,
|
|
948
|
+
display: true,
|
|
949
|
+
details: { name, workstream: ws, outcome: null, closed: false }
|
|
950
|
+
},
|
|
951
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
952
|
+
"deliver-failed"
|
|
953
|
+
)
|
|
954
|
+
).catch(() => {
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
void waiting.then(async (r) => {
|
|
959
|
+
if (inflight.get(key) !== entry2) return;
|
|
960
|
+
if (entry2.cancelling) entry2.parked = () => settle(r);
|
|
961
|
+
else await settle(r);
|
|
962
|
+
});
|
|
963
|
+
return entry2;
|
|
531
964
|
}
|
|
532
|
-
async function start(
|
|
965
|
+
async function start(call2, signal, ctx, queuedAs) {
|
|
533
966
|
let slotHeld = true;
|
|
534
967
|
const freeSlot = () => {
|
|
535
968
|
if (slotHeld) {
|
|
536
969
|
starting--;
|
|
537
|
-
|
|
970
|
+
reportPending();
|
|
538
971
|
}
|
|
539
972
|
slotHeld = false;
|
|
540
973
|
};
|
|
541
|
-
|
|
974
|
+
reportPending();
|
|
975
|
+
const ws = call2.workstream;
|
|
976
|
+
const req = call2.request;
|
|
542
977
|
try {
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
978
|
+
const keep = req.keep === true;
|
|
979
|
+
const timeoutS = req.timeoutS ?? DELEGATE_TIMEOUT_S;
|
|
980
|
+
let cwd;
|
|
981
|
+
if (req.workspace !== true) {
|
|
982
|
+
cwd = req.cwd ? req.cwd : ctx?.cwd ?? process.cwd();
|
|
983
|
+
if (!isDir(cwd))
|
|
984
|
+
throw new SpawnFail(`mu_delegate: cwd ${cwd} is not a directory`, "unsent");
|
|
985
|
+
}
|
|
986
|
+
let spawned;
|
|
987
|
+
try {
|
|
988
|
+
spawned = await spawn(call2, req.text, cwd, signal);
|
|
989
|
+
} catch (e) {
|
|
990
|
+
if (e instanceof SpawnFail && e.kind === "nomux") {
|
|
991
|
+
freeSlot();
|
|
992
|
+
return startProcess(call2);
|
|
993
|
+
}
|
|
994
|
+
const fail2 = e instanceof SpawnFail ? e : new SpawnFail(errText(e), "unsent");
|
|
995
|
+
await settleFailedStart(call2, fail2);
|
|
548
996
|
throw new Error(
|
|
549
|
-
|
|
997
|
+
fail2.kind === "unsent" ? `${fail2.message}. The call is durable and refused: reissue it with ${DELEGATE_REISSUE_TOOL} { call_id: "${call2.callId}" } (task ${ws}/${call2.task}).` : `${fail2.message}. Recorded on ${ws}/${call2.task} as failed.`
|
|
550
998
|
);
|
|
551
|
-
const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
|
|
552
|
-
let cwd;
|
|
553
|
-
if (params.workspace !== true) {
|
|
554
|
-
cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
|
|
555
|
-
if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
|
|
556
999
|
}
|
|
557
|
-
const { name, runs, attach, workspace, sessionFile } =
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
);
|
|
565
|
-
const entry = {
|
|
566
|
-
abort: new AbortController(),
|
|
567
|
-
cancelling: false,
|
|
1000
|
+
const { name, runs, attach, workspace, sessionFile, identity } = spawned;
|
|
1001
|
+
await watch(call2, name, runs, {
|
|
1002
|
+
keep,
|
|
1003
|
+
timeoutS,
|
|
1004
|
+
...queuedAs ? { queuedAs } : {},
|
|
1005
|
+
...attach ? { attach } : {},
|
|
1006
|
+
...workspace ? { workspace } : {},
|
|
568
1007
|
...sessionFile ? { sessionFile } : {},
|
|
569
|
-
...
|
|
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
|
-
);
|
|
1008
|
+
...identity ? { identity } : {},
|
|
1009
|
+
tracked: async () => {
|
|
1010
|
+
freeSlot();
|
|
1011
|
+
reportPending();
|
|
1012
|
+
await calls.mark(call2.callId, "running", { agent: name }).catch((e) => {
|
|
1013
|
+
trace("mark.running.failed", { call: call2.callId, error: errText(e) });
|
|
1014
|
+
});
|
|
632
1015
|
}
|
|
633
|
-
};
|
|
634
|
-
void waiting.then(async (r) => {
|
|
635
|
-
if (inflight.get(name) !== entry) return;
|
|
636
|
-
if (entry.cancelling) entry.parked = () => settle(r);
|
|
637
|
-
else await settle(r);
|
|
638
1016
|
});
|
|
639
1017
|
const lines = [
|
|
640
|
-
`Delegated to ${
|
|
1018
|
+
`Delegated to ${ws}/${name} (task ${ws}/${call2.task}, call ${call2.callId}). Its answer arrives with your next settle (and stays on the task: mu task notes ${call2.task} -w ${ws}); carry on, or end your turn if your next step needs it. Do not poll.`,
|
|
641
1019
|
workspace ? `Workspace: ${workspace}` : void 0,
|
|
642
1020
|
attach ? `Attach: ${attach}` : void 0,
|
|
643
1021
|
`Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
|
|
@@ -646,7 +1024,10 @@ ${task}` : task,
|
|
|
646
1024
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
647
1025
|
details: {
|
|
648
1026
|
name,
|
|
649
|
-
workstream:
|
|
1027
|
+
workstream: ws,
|
|
1028
|
+
task: `${ws}/${call2.task}`,
|
|
1029
|
+
state: "running",
|
|
1030
|
+
call_id: call2.callId,
|
|
650
1031
|
attach: attach ?? null,
|
|
651
1032
|
workspace: workspace ?? null,
|
|
652
1033
|
keep
|
|
@@ -657,11 +1038,92 @@ ${task}` : task,
|
|
|
657
1038
|
drain();
|
|
658
1039
|
}
|
|
659
1040
|
}
|
|
1041
|
+
const finishOrphan = (call2, outcome, text) => calls.finish(call2.callId, { outcome, text, author: call2.caller }).catch((e) => {
|
|
1042
|
+
trace("finishOrphan.failed", { call: call2.callId, error: errText(e) });
|
|
1043
|
+
return void 0;
|
|
1044
|
+
});
|
|
1045
|
+
function startProcess(call2) {
|
|
1046
|
+
const ws = call2.workstream;
|
|
1047
|
+
const name = call2.task;
|
|
1048
|
+
const key = inflightKey(ws, name);
|
|
1049
|
+
const entry2 = { abort: new AbortController(), call: call2, cancelling: false };
|
|
1050
|
+
inflight.set(key, entry2);
|
|
1051
|
+
reportPending();
|
|
1052
|
+
const settle = async (r) => {
|
|
1053
|
+
const o = json(r) ?? {};
|
|
1054
|
+
forget(key);
|
|
1055
|
+
if (o.outcome === void 0 || o.state === "refused") {
|
|
1056
|
+
const text = processFailedText(call2, o, why(r), r.code, DELEGATE_REISSUE_TOOL);
|
|
1057
|
+
if (o.state !== "refused") await finishOrphan(call2, "error", text);
|
|
1058
|
+
startFailed(name, new Error(text), "Delegate");
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const verdict = await calls.show(call2.callId).then((x) => x.verdict ?? void 0).catch(() => void 0);
|
|
1062
|
+
owed.set(call2.callId, {
|
|
1063
|
+
callId: call2.callId,
|
|
1064
|
+
name,
|
|
1065
|
+
workstream: ws,
|
|
1066
|
+
task: call2.task,
|
|
1067
|
+
outcome: o.outcome,
|
|
1068
|
+
...verdict ? { verdict } : {},
|
|
1069
|
+
...call2.record && o.state === "done" ? { record: `${call2.record.workstream}/${call2.record.task}` } : {},
|
|
1070
|
+
at: Date.now()
|
|
1071
|
+
});
|
|
1072
|
+
wakeIfIdle();
|
|
1073
|
+
};
|
|
1074
|
+
const running = mu(
|
|
1075
|
+
["delegates", "run", call2.callId, "--no-keep", "--json"],
|
|
1076
|
+
entry2.abort.signal,
|
|
1077
|
+
void 0,
|
|
1078
|
+
{ drainMs: 2e4 }
|
|
1079
|
+
);
|
|
1080
|
+
entry2.process = running;
|
|
1081
|
+
void running.then(async (r) => {
|
|
1082
|
+
if (inflight.get(key) !== entry2) return;
|
|
1083
|
+
await settle(r);
|
|
1084
|
+
});
|
|
1085
|
+
return processStarted(call2, DELEGATE_CANCEL_TOOL);
|
|
1086
|
+
}
|
|
1087
|
+
async function settleFailedStart(call2, fail2) {
|
|
1088
|
+
try {
|
|
1089
|
+
if (fail2.kind === "unsent") await calls.mark(call2.callId, "refused", { error: fail2.message });
|
|
1090
|
+
else
|
|
1091
|
+
await calls.finish(call2.callId, {
|
|
1092
|
+
outcome: fail2.kind === "aborted" ? "cancelled" : fail2.kind === "unknown" ? "unknown" : "error",
|
|
1093
|
+
text: fail2.message,
|
|
1094
|
+
author: call2.caller
|
|
1095
|
+
});
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
trace("settleFailedStart.failed", { call: call2.callId, error: errText(e) });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const callIdIn = (name) => name.startsWith("pi:");
|
|
1101
|
+
const admission = () => {
|
|
1102
|
+
const max = delegateMax();
|
|
1103
|
+
if (inflight.size + starting < max) return "starting";
|
|
1104
|
+
return queue.length < DELEGATE_QUEUE_FACTOR * max ? "queued" : "refused";
|
|
1105
|
+
};
|
|
1106
|
+
const queuedResult = (call2, state, handle, position) => {
|
|
1107
|
+
const max = delegateMax();
|
|
1108
|
+
const taskRef = `${call2.workstream}/${call2.task}`;
|
|
1109
|
+
const text = state === "refused" ? `REFUSED, not started: ${inflight.size + starting} running and ${queue.length} queued (MU_DELEGATE_MAX=${max}). The call is durable (task ${taskRef}, call ${call2.callId}) and will NOT start on its own. Reissue it once slots free, as answers arrive: ${DELEGATE_REISSUE_TOOL} { call_id: "${call2.callId}" }. List: mu delegates.` : `Queued as ${handle} (#${position}; ${inflight.size + starting} running, MU_DELEGATE_MAX=${max}; task ${taskRef}, call ${call2.callId}). It starts when a slot frees, and its answer arrives with a settle like any delegate's; do not poll. Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${handle}" }`;
|
|
1110
|
+
return {
|
|
1111
|
+
content: [{ type: "text", text }],
|
|
1112
|
+
details: {
|
|
1113
|
+
name: handle,
|
|
1114
|
+
workstream: call2.workstream,
|
|
1115
|
+
task: taskRef,
|
|
1116
|
+
state,
|
|
1117
|
+
call_id: call2.callId,
|
|
1118
|
+
...state === "queued" ? { queued: true, position } : { refused: true }
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
};
|
|
660
1122
|
pi.registerTool({
|
|
661
1123
|
name: DELEGATE_TOOL,
|
|
662
1124
|
label: "mu delegate",
|
|
663
|
-
description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation, or a refuter of a claim, plan or fix before you act on it; call it several times to fan out in parallel (at most ${delegateMax()} run at once; up to ${DELEGATE_QUEUE_FACTOR * delegateMax()} more are queued and start as slots free). The subagent starts with no context, in your working directory: put everything it needs in task. The call returns at once and the answer arrives later
|
|
664
|
-
promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later
|
|
1125
|
+
description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream; with no tmux or herdr, a child pi process with no pane) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation, or a refuter of a claim, plan or fix before you act on it; call it several times to fan out in parallel (at most ${delegateMax()} run at once; up to ${DELEGATE_QUEUE_FACTOR * delegateMax()} more are queued and start as slots free; past that a call is refused: durable, reissue it with ${DELEGATE_REISSUE_TOOL}). Every call is a mu task (kind delegate): the result names it, and the full answer stays on it. The subagent starts with no context, in your working directory: put everything it needs in task. The call returns at once and the answer arrives later, with a settle (a wave's as ONE summary): carry on with other work, or end your turn if your next step needs the answer (it resumes you). The pane closes after a clean finish; keep: true leaves it open (needs a mux). When the check judges a mu task, set record so its verdict lands on that task; leave it out when there is no task. Set wave to group a fan-out (mu delegates tally).`,
|
|
1126
|
+
promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later, with a settle",
|
|
665
1127
|
parameters: {
|
|
666
1128
|
type: "object",
|
|
667
1129
|
properties: {
|
|
@@ -676,16 +1138,16 @@ ${task}` : task,
|
|
|
676
1138
|
},
|
|
677
1139
|
cwd: {
|
|
678
1140
|
type: "string",
|
|
679
|
-
description: "Working directory for the delegate (default: yours;
|
|
1141
|
+
description: "Working directory for the delegate (default: yours); with workspace, the git repo the worktree is made from"
|
|
680
1142
|
},
|
|
681
1143
|
timeout: {
|
|
682
1144
|
type: "number",
|
|
683
1145
|
exclusiveMinimum: 0,
|
|
684
|
-
description: `Seconds to wait for the answer (default ${DELEGATE_TIMEOUT_S}). On timeout you get a "still running"
|
|
1146
|
+
description: `Seconds to wait for the answer (default ${DELEGATE_TIMEOUT_S}). On timeout you get a "still running" answer instead, and the real answer will not arrive later; the subagent keeps running in its pane.`
|
|
685
1147
|
},
|
|
686
1148
|
workspace: {
|
|
687
1149
|
type: "boolean",
|
|
688
|
-
description: "For tasks that edit files: its own
|
|
1150
|
+
description: "For tasks that edit files: its own git worktree branched from the current commit of the repo at cwd (default: yours). The result names the path; changes stay there until you merge them. This isolates repository edits only: the subagent runs as you, with your files, environment and network."
|
|
689
1151
|
},
|
|
690
1152
|
cli: {
|
|
691
1153
|
type: "string",
|
|
@@ -697,24 +1159,31 @@ ${task}` : task,
|
|
|
697
1159
|
},
|
|
698
1160
|
keep: {
|
|
699
1161
|
type: "boolean",
|
|
700
|
-
description: "Keep the pane after it finishes, to talk to it again"
|
|
1162
|
+
description: "Keep the pane after it finishes, to talk to it again (needs a mux; ignored without one)"
|
|
701
1163
|
},
|
|
702
1164
|
record: {
|
|
703
1165
|
type: "object",
|
|
704
|
-
description: "When the answer arrives, write its VERDICT/EVIDENCE onto this task as a note (for a check that judges a mu task; omit with no task); task
|
|
1166
|
+
description: "When the answer arrives, write its VERDICT/EVIDENCE onto this task as a note (for a check that judges a mu task; omit with no task); name the workstream: task as <ws>/<task>, or set workstream.",
|
|
705
1167
|
properties: {
|
|
706
1168
|
task: { type: "string" },
|
|
707
1169
|
workstream: { type: "string" }
|
|
708
1170
|
},
|
|
709
1171
|
required: ["task"],
|
|
710
1172
|
additionalProperties: false
|
|
1173
|
+
},
|
|
1174
|
+
wave: {
|
|
1175
|
+
type: "string",
|
|
1176
|
+
description: "Label grouping the calls of one fan-out; `mu delegates tally -w <workstream> --wave <label>` counts their verdicts per judged task."
|
|
1177
|
+
},
|
|
1178
|
+
workstream: {
|
|
1179
|
+
type: "string",
|
|
1180
|
+
description: `Workstream that holds the delegate task and runs the delegate (default: ${W}).`
|
|
711
1181
|
}
|
|
712
1182
|
},
|
|
713
1183
|
required: ["task"],
|
|
714
1184
|
additionalProperties: false
|
|
715
1185
|
},
|
|
716
1186
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
717
|
-
track(ctx);
|
|
718
1187
|
const task = typeof params.task === "string" ? params.task.trim() : "";
|
|
719
1188
|
if (!task) throw new Error("mu_delegate needs a non-empty task");
|
|
720
1189
|
const t = params.timeout;
|
|
@@ -722,179 +1191,435 @@ ${task}` : task,
|
|
|
722
1191
|
throw new Error(
|
|
723
1192
|
`mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
|
|
724
1193
|
);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1194
|
+
const callCwd = typeof params.cwd === "string" && params.cwd ? params.cwd : params.workspace !== true ? ctx?.cwd ?? process.cwd() : void 0;
|
|
1195
|
+
if (callCwd !== void 0 && !isDir(callCwd))
|
|
1196
|
+
throw new Error(`mu_delegate: cwd ${callCwd} is not a directory`);
|
|
1197
|
+
let model;
|
|
1198
|
+
if (params.model !== void 0 && params.model !== "") {
|
|
1199
|
+
model = normalizeModel(params.model);
|
|
1200
|
+
if (!model)
|
|
1201
|
+
throw new Error(
|
|
1202
|
+
`mu_delegate: model must be one pi model id like 'provider/id:thinking', as listed by \`pi --list-models\` (got ${JSON.stringify(params.model)}); omit it for the cli's default`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
let workstream = W;
|
|
1206
|
+
if (params.workstream !== void 0) {
|
|
1207
|
+
const problem = workstreamNameProblem(
|
|
1208
|
+
typeof params.workstream === "string" ? params.workstream : String(params.workstream),
|
|
1209
|
+
"workstream"
|
|
1210
|
+
);
|
|
1211
|
+
if (problem !== void 0) throw new Error(`mu_delegate: ${problem} Nothing spawned.`);
|
|
1212
|
+
workstream = params.workstream;
|
|
1213
|
+
}
|
|
1214
|
+
if (params.wave !== void 0 && (typeof params.wave !== "string" || params.wave.trim() === ""))
|
|
1215
|
+
throw new Error(
|
|
1216
|
+
`mu_delegate: wave must be a non-empty label (got ${JSON.stringify(params.wave)})`
|
|
1217
|
+
);
|
|
1218
|
+
const record = await resolveRecord(params.record);
|
|
1219
|
+
const brief = typeof params.brief === "string" ? params.brief.trim() : "";
|
|
1220
|
+
const label = typeof params.label === "string" && params.label.trim() ? params.label.trim() : void 0;
|
|
1221
|
+
const decision = admission();
|
|
1222
|
+
const handle = decision === "starting" ? "" : `queued-${++queueSeq}`;
|
|
1223
|
+
if (decision === "starting") starting++;
|
|
1224
|
+
const session = piSessionId(ctx) || process.env.PI_SESSION_ID || `local-${token}`;
|
|
1225
|
+
const caller = `pi:${session}`;
|
|
1226
|
+
const info = {
|
|
1227
|
+
callId: `${caller}:${token}-${++callSeq}`,
|
|
1228
|
+
caller,
|
|
1229
|
+
workstream,
|
|
1230
|
+
...typeof params.wave === "string" ? { wave: params.wave.trim() } : {},
|
|
1231
|
+
...record ? { record } : {},
|
|
1232
|
+
request: {
|
|
1233
|
+
text: brief ? `${brief}
|
|
1234
|
+
|
|
1235
|
+
${task}` : task,
|
|
1236
|
+
...label ? { label } : {},
|
|
1237
|
+
...typeof params.cli === "string" && params.cli ? { cli: params.cli } : {},
|
|
1238
|
+
...model ? { model } : {},
|
|
1239
|
+
...callCwd !== void 0 ? { cwd: callCwd } : {},
|
|
1240
|
+
...params.workspace === true ? { workspace: true } : {},
|
|
1241
|
+
...typeof t === "number" ? { timeoutS: Math.ceil(t) } : {},
|
|
1242
|
+
...params.keep === true ? { keep: true } : {}
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
if (decision === "starting") {
|
|
1246
|
+
let reg;
|
|
1247
|
+
try {
|
|
1248
|
+
reg = await calls.register(info, "starting");
|
|
1249
|
+
} catch (e) {
|
|
1250
|
+
starting--;
|
|
1251
|
+
reportPending();
|
|
1252
|
+
drain();
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
`mu_delegate: could not register the call: ${errText(e)}. Nothing spawned.`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
return start(reg.call, signal, ctx);
|
|
728
1258
|
}
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
1259
|
+
if (decision === "refused") {
|
|
1260
|
+
let reg;
|
|
1261
|
+
try {
|
|
1262
|
+
reg = await calls.register(info, "refused");
|
|
1263
|
+
} catch (e) {
|
|
732
1264
|
throw new Error(
|
|
733
|
-
`mu_delegate:
|
|
1265
|
+
`mu_delegate: could not register the call: ${errText(e)}. Nothing spawned.`
|
|
734
1266
|
);
|
|
735
|
-
|
|
1267
|
+
}
|
|
1268
|
+
return queuedResult(reg.call, "refused", handle, 0);
|
|
736
1269
|
}
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1270
|
+
const entry2 = {
|
|
1271
|
+
handle,
|
|
1272
|
+
call: { ...info, task: "" },
|
|
1273
|
+
ctx,
|
|
1274
|
+
registered: Promise.resolve()
|
|
1275
|
+
};
|
|
1276
|
+
entry2.registered = calls.register(info, "queued").then((reg) => {
|
|
1277
|
+
entry2.call = reg.call;
|
|
1278
|
+
});
|
|
1279
|
+
queue.push(entry2);
|
|
1280
|
+
reportPending();
|
|
1281
|
+
try {
|
|
1282
|
+
await entry2.registered;
|
|
1283
|
+
} catch (e) {
|
|
1284
|
+
const at = queue.indexOf(entry2);
|
|
1285
|
+
if (at >= 0) queue.splice(at, 1);
|
|
1286
|
+
reportPending();
|
|
1287
|
+
throw new Error(`mu_delegate: could not register the call: ${errText(e)}. Nothing queued.`);
|
|
742
1288
|
}
|
|
743
|
-
|
|
1289
|
+
return queuedResult(entry2.call, "queued", handle, queue.indexOf(entry2) + 1 || queue.length);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
pi.registerTool({
|
|
1293
|
+
name: DELEGATE_REISSUE_TOOL,
|
|
1294
|
+
label: "mu delegate reissue",
|
|
1295
|
+
description: `Run a call ${DELEGATE_TOOL} refused (the queue was full): same call, same task, never a second verdict. Takes the call_id from the refusal; starts now if a slot is free, else queues.`,
|
|
1296
|
+
parameters: {
|
|
1297
|
+
type: "object",
|
|
1298
|
+
properties: { call_id: { type: "string", description: "The refused call's id" } },
|
|
1299
|
+
required: ["call_id"],
|
|
1300
|
+
additionalProperties: false
|
|
1301
|
+
},
|
|
1302
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1303
|
+
const callId = typeof params.call_id === "string" ? params.call_id : "";
|
|
1304
|
+
if (!callId) throw new Error(`${DELEGATE_REISSUE_TOOL} needs a call_id`);
|
|
1305
|
+
const shown = await calls.show(callId).catch((e) => {
|
|
1306
|
+
throw new Error(`${DELEGATE_REISSUE_TOOL}: ${errText(e)}`);
|
|
1307
|
+
});
|
|
1308
|
+
if (shown.state !== "refused")
|
|
1309
|
+
throw new Error(`${DELEGATE_REISSUE_TOOL}: call ${callId} is ${shown.state}, not refused.`);
|
|
1310
|
+
const decision = admission();
|
|
1311
|
+
if (decision === "refused")
|
|
744
1312
|
throw new Error(
|
|
745
|
-
|
|
1313
|
+
`${DELEGATE_REISSUE_TOOL}: still full (${inflight.size + starting} running, ${queue.length} queued). Nothing changed; reissue ${callId} when an answer arrives.`
|
|
746
1314
|
);
|
|
1315
|
+
if (decision === "starting") starting++;
|
|
1316
|
+
try {
|
|
1317
|
+
await calls.mark(callId, decision === "starting" ? "starting" : "queued");
|
|
1318
|
+
} catch (e) {
|
|
1319
|
+
if (decision === "starting") {
|
|
1320
|
+
starting--;
|
|
1321
|
+
reportPending();
|
|
1322
|
+
drain();
|
|
1323
|
+
}
|
|
1324
|
+
throw new Error(`${DELEGATE_REISSUE_TOOL}: ${errText(e)}`);
|
|
1325
|
+
}
|
|
1326
|
+
if (decision === "starting") return start(shown.call, signal, ctx);
|
|
747
1327
|
const handle = `queued-${++queueSeq}`;
|
|
748
|
-
queue.push({
|
|
749
|
-
|
|
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
|
-
};
|
|
1328
|
+
queue.push({ handle, call: shown.call, ctx, registered: Promise.resolve() });
|
|
1329
|
+
reportPending();
|
|
1330
|
+
return queuedResult(shown.call, "queued", handle, queue.length);
|
|
767
1331
|
}
|
|
768
1332
|
});
|
|
769
1333
|
pi.registerTool({
|
|
770
1334
|
name: DELEGATE_CANCEL_TOOL,
|
|
771
1335
|
label: "mu delegate cancel",
|
|
772
|
-
description: `Stop a delegate started by ${DELEGATE_TOOL}: abort its turn and close its pane (keep: true leaves the pane open).
|
|
1336
|
+
description: `Stop a delegate started by ${DELEGATE_TOOL}: abort its turn and close its pane (keep: true leaves the pane open; a mux-free one's child pi is stopped). A queued or refused call is cancelled too (name it by its handle or call_id). The verdict target, if any, gets "no verdict (cancelled)". Its answer is not shown afterwards.`,
|
|
773
1337
|
parameters: {
|
|
774
1338
|
type: "object",
|
|
775
1339
|
properties: {
|
|
776
|
-
name: {
|
|
1340
|
+
name: {
|
|
1341
|
+
type: "string",
|
|
1342
|
+
description: "Delegate name (delegate-1), queue handle (queued-2) or call_id"
|
|
1343
|
+
},
|
|
777
1344
|
keep: { type: "boolean", description: "Leave the pane open after aborting" }
|
|
778
1345
|
},
|
|
779
1346
|
required: ["name"],
|
|
780
1347
|
additionalProperties: false
|
|
781
1348
|
},
|
|
782
|
-
async execute(_id, params
|
|
783
|
-
track(ctx);
|
|
1349
|
+
async execute(_id, params) {
|
|
784
1350
|
const name = typeof params.name === "string" ? params.name : "";
|
|
785
1351
|
if (!name) throw new Error("mu_delegate_cancel needs a name");
|
|
786
|
-
const
|
|
1352
|
+
const cancelCall = async (call2, author, text2) => {
|
|
1353
|
+
try {
|
|
1354
|
+
const f = await calls.finish(call2.callId, {
|
|
1355
|
+
outcome: "cancelled",
|
|
1356
|
+
...text2 ? { text: text2 } : {},
|
|
1357
|
+
author
|
|
1358
|
+
});
|
|
1359
|
+
if (f.recorded && call2.record)
|
|
1360
|
+
return ` Recorded on ${call2.record.workstream}/${call2.record.task} as a note (no verdict: cancelled).`;
|
|
1361
|
+
return f.changed ? ` Task ${call2.workstream}/${call2.task} closed (cancelled).` : "";
|
|
1362
|
+
} catch (e) {
|
|
1363
|
+
return ` Recording the cancel failed: ${errText(e)}.`;
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
const qi = queue.findIndex((q) => q.handle === name || q.call.callId === name);
|
|
787
1367
|
if (qi >= 0) {
|
|
788
1368
|
const [q] = queue.splice(qi, 1);
|
|
789
|
-
|
|
790
|
-
|
|
1369
|
+
reportPending();
|
|
1370
|
+
let recorded = "";
|
|
1371
|
+
if (q) {
|
|
1372
|
+
await q.registered.catch(() => {
|
|
1373
|
+
});
|
|
1374
|
+
recorded = await cancelCall(q.call, q.handle);
|
|
1375
|
+
}
|
|
791
1376
|
return {
|
|
792
1377
|
content: [
|
|
793
1378
|
{ type: "text", text: `Dropped ${name} from the queue; it never started.${recorded}` }
|
|
794
1379
|
],
|
|
795
|
-
details: { name, workstream: W }
|
|
1380
|
+
details: { name, workstream: q?.call.workstream ?? W }
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
const found = [...inflight].find(
|
|
1384
|
+
([key2, e]) => key2 === name || key2 === inflightKey(e.call.workstream, name) || e.call.callId === name
|
|
1385
|
+
);
|
|
1386
|
+
if (!found && callIdIn(name)) {
|
|
1387
|
+
const shown = await calls.show(name).catch((e) => {
|
|
1388
|
+
throw new Error(`mu_delegate_cancel: ${errText(e)}`);
|
|
1389
|
+
});
|
|
1390
|
+
const recorded = await cancelCall(shown.call, name);
|
|
1391
|
+
return {
|
|
1392
|
+
content: [{ type: "text", text: `Cancelled ${name} (${shown.state}).${recorded}` }],
|
|
1393
|
+
details: { name, workstream: shown.call.workstream }
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
const key = found?.[0];
|
|
1397
|
+
const entry2 = found?.[1];
|
|
1398
|
+
const ws = entry2?.call.workstream ?? W;
|
|
1399
|
+
const agentName = key ? key.slice(key.indexOf("/") + 1) : name;
|
|
1400
|
+
if (entry2?.process && key) {
|
|
1401
|
+
entry2.abort.abort();
|
|
1402
|
+
forget(key);
|
|
1403
|
+
await entry2.process;
|
|
1404
|
+
await finishOrphan(entry2.call, "cancelled", "cancelled");
|
|
1405
|
+
const shown = await calls.show(entry2.call.callId).catch(() => void 0);
|
|
1406
|
+
const where = entry2.call.record ? ` Recorded on ${entry2.call.record.workstream}/${entry2.call.record.task} as a note (no verdict: cancelled).` : "";
|
|
1407
|
+
return {
|
|
1408
|
+
content: [
|
|
1409
|
+
{
|
|
1410
|
+
type: "text",
|
|
1411
|
+
text: `Cancelled ${ws}/${agentName} (child pi stopped; call ${shown?.state ?? "cancelling"}).${where}`
|
|
1412
|
+
}
|
|
1413
|
+
],
|
|
1414
|
+
details: { name: agentName, workstream: ws }
|
|
796
1415
|
};
|
|
797
1416
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const ab = await mu(["agent", "abort", name, "-w", W]);
|
|
1417
|
+
if (entry2) entry2.cancelling = true;
|
|
1418
|
+
const ab = await mu(["agent", "abort", agentName, "-w", ws]);
|
|
801
1419
|
if (ab.code !== 0) {
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
const parked =
|
|
805
|
-
|
|
1420
|
+
if (entry2) {
|
|
1421
|
+
entry2.cancelling = false;
|
|
1422
|
+
const parked = entry2.parked;
|
|
1423
|
+
entry2.parked = void 0;
|
|
806
1424
|
if (parked) void parked();
|
|
807
1425
|
}
|
|
808
|
-
throw failure(`mu agent abort ${
|
|
1426
|
+
throw failure(`mu agent abort ${ws}/${agentName}`, ab);
|
|
809
1427
|
}
|
|
810
|
-
if (
|
|
811
|
-
|
|
812
|
-
forget(
|
|
1428
|
+
if (entry2 && key) {
|
|
1429
|
+
entry2.abort.abort();
|
|
1430
|
+
forget(key);
|
|
813
1431
|
}
|
|
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))}`;
|
|
1432
|
+
let text = `Aborted ${ws}/${agentName}.`;
|
|
1433
|
+
const sessionFile = entry2?.sessionFile;
|
|
1434
|
+
const evidence = evidenceLines({ ...sessionFile ? { sessionFile } : {} });
|
|
1435
|
+
if (entry2)
|
|
1436
|
+
text += await cancelCall(
|
|
1437
|
+
entry2.call,
|
|
1438
|
+
agentName,
|
|
1439
|
+
sessionFile ? lastAssistantTextIn(sessionFile) : void 0
|
|
1440
|
+
);
|
|
824
1441
|
if (params.keep === true) text += " Pane kept.";
|
|
825
1442
|
else {
|
|
826
|
-
const c = await
|
|
827
|
-
text += c.
|
|
1443
|
+
const c = await closeDelegate(agentName, entry2?.identity, ws);
|
|
1444
|
+
text += c.closed ? " Pane closed." : ` Pane kept: ${c.why}`;
|
|
828
1445
|
}
|
|
829
1446
|
if (evidence.length > 0) text += `
|
|
830
1447
|
${evidence.join("\n")}`;
|
|
831
|
-
return { content: [{ type: "text", text }], details: { name, workstream:
|
|
1448
|
+
return { content: [{ type: "text", text }], details: { name: agentName, workstream: ws } };
|
|
832
1449
|
}
|
|
833
1450
|
});
|
|
834
1451
|
pi.on("message_start", (e) => {
|
|
835
1452
|
const m = e.message;
|
|
836
1453
|
if (m?.role !== "custom" || m.customType !== DELEGATE_MESSAGE_TYPE) return;
|
|
837
|
-
const id = m.details?.
|
|
838
|
-
if (typeof id
|
|
1454
|
+
const id = m.details?.wake;
|
|
1455
|
+
if (typeof id !== "number") return;
|
|
1456
|
+
for (const c of arriving.get(id)?.calls ?? []) owed.delete(c);
|
|
1457
|
+
arriving.delete(id);
|
|
839
1458
|
});
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
if (!a
|
|
1459
|
+
const dropWakes = (ids, also = []) => {
|
|
1460
|
+
const names = new Set(also);
|
|
1461
|
+
for (const id of ids) {
|
|
1462
|
+
const a = arriving.get(id);
|
|
1463
|
+
if (!a) continue;
|
|
845
1464
|
arriving.delete(id);
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1465
|
+
for (const o of a.opts ?? []) o.triggerTurn = false;
|
|
1466
|
+
for (const c of a.calls) names.add(c);
|
|
1467
|
+
}
|
|
1468
|
+
const dropped = [];
|
|
1469
|
+
for (const c of names) {
|
|
1470
|
+
const a = owed.get(c);
|
|
1471
|
+
if (!a) continue;
|
|
1472
|
+
owed.delete(c);
|
|
1473
|
+
dropped.push(a);
|
|
853
1474
|
}
|
|
1475
|
+
trace("dropped", { calls: dropped.map((d) => d.callId) });
|
|
854
1476
|
if (dropped.length === 0) return;
|
|
855
|
-
showStatus();
|
|
856
1477
|
void Promise.resolve(
|
|
857
|
-
|
|
1478
|
+
send(
|
|
858
1479
|
{
|
|
859
1480
|
customType: DELEGATE_MESSAGE_TYPE,
|
|
860
1481
|
content: droppedNote(dropped),
|
|
861
1482
|
display: true,
|
|
862
|
-
details: { dropped }
|
|
1483
|
+
details: { dropped: dropped.map((d) => d.callId) }
|
|
863
1484
|
},
|
|
864
|
-
{ triggerTurn: false }
|
|
1485
|
+
{ triggerTurn: false },
|
|
1486
|
+
"dropped-note"
|
|
865
1487
|
)
|
|
866
1488
|
).catch(() => {
|
|
867
1489
|
});
|
|
1490
|
+
};
|
|
1491
|
+
const recoverLost = (candidates, ctx) => {
|
|
1492
|
+
settleCheck = void 0;
|
|
1493
|
+
if (closed) return;
|
|
1494
|
+
let idle = false;
|
|
1495
|
+
try {
|
|
1496
|
+
idle = ctx.isIdle?.() ?? true;
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
if (!idle) return;
|
|
1500
|
+
if ([...arriving].some(([id, a2]) => a2.sent && !candidates.includes(id))) return;
|
|
1501
|
+
const lost = candidates.filter((id) => arriving.get(id)?.sent);
|
|
1502
|
+
dropWakes(lost.filter((id) => arriving.get(id)?.resent));
|
|
1503
|
+
const next = lost.find((id) => arriving.has(id));
|
|
1504
|
+
const a = next === void 0 ? void 0 : arriving.get(next);
|
|
1505
|
+
if (next === void 0 || !a?.message) {
|
|
1506
|
+
wake("wake-after-settle");
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
a.resent = true;
|
|
1510
|
+
trace("resend", { wake: next, calls: a.calls });
|
|
1511
|
+
void Promise.resolve(
|
|
1512
|
+
send(a.message, { deliverAs: "followUp", triggerTurn: true }, "resend")
|
|
1513
|
+
).catch(() => {
|
|
1514
|
+
arriving.delete(next);
|
|
1515
|
+
});
|
|
1516
|
+
};
|
|
1517
|
+
pi.on("agent_start", () => {
|
|
1518
|
+
busy = true;
|
|
1519
|
+
});
|
|
1520
|
+
pi.on("agent_settled", (e, ctx) => {
|
|
1521
|
+
busy = false;
|
|
1522
|
+
const sent = [...arriving].filter(([, a]) => a.sent).map(([id]) => id);
|
|
1523
|
+
if (e.aborted === true) {
|
|
1524
|
+
clearImmediate(settleCheck);
|
|
1525
|
+
dropWakes(sent, [...owed.keys()]);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (sent.length === 0 && owed.size === 0) return;
|
|
1529
|
+
clearImmediate(settleCheck);
|
|
1530
|
+
settleCheck = setImmediate(() => recoverLost(sent, ctx));
|
|
868
1531
|
});
|
|
869
|
-
pi.on("
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1532
|
+
pi.on("session_start", async (_e, ctx) => {
|
|
1533
|
+
const session = piSessionId(ctx) || process.env.PI_SESSION_ID;
|
|
1534
|
+
if (!session || closed) return;
|
|
1535
|
+
const r = await mu(["delegates", "list", "--caller", `pi:${session}`, "--json"]).catch(
|
|
1536
|
+
() => void 0
|
|
1537
|
+
);
|
|
1538
|
+
const items = (r ? json(r)?.items : void 0) ?? [];
|
|
1539
|
+
for (const row of items) {
|
|
1540
|
+
if (row.state !== "running" || typeof row.agent !== "string" || row.agent === row.task)
|
|
1541
|
+
continue;
|
|
1542
|
+
const call2 = callInfoOf(row);
|
|
1543
|
+
if (call2.caller !== `pi:${session}` || inflight.has(inflightKey(call2.workstream, row.agent)))
|
|
1544
|
+
continue;
|
|
1545
|
+
await watch(call2, row.agent, 0, {
|
|
1546
|
+
keep: call2.request.keep === true,
|
|
1547
|
+
timeoutS: call2.request.timeoutS ?? DELEGATE_TIMEOUT_S
|
|
1548
|
+
});
|
|
873
1549
|
}
|
|
1550
|
+
});
|
|
1551
|
+
pi.on("session_shutdown", async (e, ctx) => {
|
|
1552
|
+
closed = true;
|
|
1553
|
+
clearImmediate(settleCheck);
|
|
1554
|
+
clearImmediate(wakeSoon);
|
|
1555
|
+
trace("session_shutdown.state", {
|
|
1556
|
+
arriving: [...arriving.values()].map((a) => ({ calls: a.calls, sent: a.sent })),
|
|
1557
|
+
owed: [...owed.keys()],
|
|
1558
|
+
inflight: [...inflight.keys()]
|
|
1559
|
+
});
|
|
1560
|
+
arriving.clear();
|
|
1561
|
+
owed.clear();
|
|
874
1562
|
if (inflight.size === 0 && queue.length === 0) return;
|
|
875
|
-
const
|
|
1563
|
+
const shown = (key) => key.startsWith(`${W}/`) ? key.slice(W.length + 1) : key;
|
|
1564
|
+
const names = [...inflight].flatMap(([k, e2]) => e2.process ? [] : [shown(k)]);
|
|
1565
|
+
const procs = [...inflight].flatMap(([k, e2]) => e2.process ? [shown(k)] : []);
|
|
876
1566
|
const sessions = [...inflight].flatMap(
|
|
877
|
-
([
|
|
1567
|
+
([k, e2]) => e2.sessionFile ? [`${shown(k)}: ${e2.sessionFile}`] : []
|
|
878
1568
|
);
|
|
879
|
-
const
|
|
880
|
-
|
|
1569
|
+
const dropped = queue.splice(0);
|
|
1570
|
+
const reason2 = e?.reason;
|
|
1571
|
+
const replacing = reason2 === "reload" || reason2 === "new" || reason2 === "resume" || reason2 === "fork";
|
|
1572
|
+
const stopping = [];
|
|
1573
|
+
for (const f of inflight.values())
|
|
1574
|
+
if (!f.process || !replacing) {
|
|
1575
|
+
f.abort.abort();
|
|
1576
|
+
const call2 = f.call;
|
|
1577
|
+
if (f.process)
|
|
1578
|
+
stopping.push(f.process.then(() => finishOrphan(call2, "cancelled", "pi quit")));
|
|
1579
|
+
}
|
|
881
1580
|
inflight.clear();
|
|
882
|
-
|
|
883
|
-
|
|
1581
|
+
reportPending();
|
|
1582
|
+
await Promise.all(stopping);
|
|
1583
|
+
const refused = [];
|
|
1584
|
+
await Promise.all(
|
|
1585
|
+
dropped.map(async (q) => {
|
|
1586
|
+
try {
|
|
1587
|
+
await q.registered;
|
|
1588
|
+
await calls.mark(q.call.callId, "refused", {
|
|
1589
|
+
error: "its session ended before it started"
|
|
1590
|
+
});
|
|
1591
|
+
refused.push(q.call.callId);
|
|
1592
|
+
} catch {
|
|
1593
|
+
}
|
|
1594
|
+
})
|
|
1595
|
+
);
|
|
884
1596
|
const parts = [];
|
|
885
1597
|
if (names.length > 0)
|
|
886
1598
|
parts.push(
|
|
887
|
-
`${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`
|
|
1599
|
+
`${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. ${reason2 === "reload" ? "This session's next runtime re-adopts them and delivers their answers." : `Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`}`
|
|
1600
|
+
);
|
|
1601
|
+
if (procs.length > 0)
|
|
1602
|
+
parts.push(
|
|
1603
|
+
replacing ? `${procs.length} mux-free delegate${procs.length === 1 ? "" : "s"} still running: ${procs.join(", ")}. Each finishes onto its task (mu task notes <name> -w ${W}); the answer will not come back here.` : `${procs.length} mux-free delegate${procs.length === 1 ? "" : "s"} cancelled: ${procs.join(", ")}.`
|
|
888
1604
|
);
|
|
889
1605
|
if (sessions.length > 0) parts.push(`Sessions: ${sessions.join("; ")}.`);
|
|
890
|
-
if (
|
|
891
|
-
parts.push(
|
|
1606
|
+
if (dropped.length > 0)
|
|
1607
|
+
parts.push(
|
|
1608
|
+
`${dropped.length} queued, never started: ${dropped.map((q) => q.handle).join(", ")}. Re-issue them${refused.length > 0 ? ` (durable and refused: ${DELEGATE_REISSUE_TOOL} in a new pi session; list: mu delegates --all)` : ""}.`
|
|
1609
|
+
);
|
|
892
1610
|
const msg = parts.join(" ");
|
|
893
1611
|
if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
|
|
894
1612
|
else process.stderr.write(`mu: ${msg}
|
|
895
1613
|
`);
|
|
896
1614
|
});
|
|
897
|
-
return {
|
|
1615
|
+
return {
|
|
1616
|
+
answerPending: () => arriving.size > 0,
|
|
1617
|
+
delivered(shown) {
|
|
1618
|
+
if (Array.isArray(shown)) for (const c of shown) owed.delete(c);
|
|
1619
|
+
else
|
|
1620
|
+
for (const [c, a] of owed) if (a.at < shown.before) owed.delete(c);
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
898
1623
|
}
|
|
899
1624
|
|
|
900
1625
|
// extension/nudge.ts
|
|
@@ -903,14 +1628,12 @@ import { join as join3 } from "path";
|
|
|
903
1628
|
var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
|
|
904
1629
|
var NUDGE_LOG_KIND = "nudge";
|
|
905
1630
|
var CLOSE_NUDGE_MESSAGE_TYPE = "mu-close-task";
|
|
906
|
-
var REFUTE_NUDGE_MESSAGE_TYPE = "mu-refute-brief";
|
|
907
1631
|
var MARK_START = "<!-- mu:keep-driving -->";
|
|
908
1632
|
var MARK_END = "<!-- /mu:keep-driving -->";
|
|
909
|
-
var WAIT_HINT_MAX = 8;
|
|
910
1633
|
var notNested = () => false;
|
|
911
1634
|
var noHold = () => void 0;
|
|
912
1635
|
function withQueuedInput(hold) {
|
|
913
|
-
return (ctx) => ctx?.hasPendingMessages?.() ? "
|
|
1636
|
+
return (ctx) => ctx?.hasPendingMessages?.() ? "input" : hold(ctx);
|
|
914
1637
|
}
|
|
915
1638
|
function noUI(ctx) {
|
|
916
1639
|
return ctx?.hasUI === false;
|
|
@@ -932,190 +1655,13 @@ function readSkill() {
|
|
|
932
1655
|
}
|
|
933
1656
|
return void 0;
|
|
934
1657
|
}
|
|
935
|
-
|
|
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");
|
|
1658
|
+
async function allItems(run, argv, uncapped) {
|
|
1659
|
+
const r = await run([...argv, uncapped]);
|
|
1660
|
+
if (r.code !== 0) return void 0;
|
|
1661
|
+
const data = parseEnvelope(r.stdout);
|
|
1662
|
+
if (data === void 0 || !Array.isArray(data.items)) return void 0;
|
|
1663
|
+
if ((data.more ?? 0) > 0) return void 0;
|
|
1664
|
+
return data.items;
|
|
1119
1665
|
}
|
|
1120
1666
|
function breadcrumb(run, args) {
|
|
1121
1667
|
void run(args).catch(() => {
|
|
@@ -1149,7 +1695,8 @@ function parallelSettle(pi, hold = noHold) {
|
|
|
1149
1695
|
const late = hold(ctx);
|
|
1150
1696
|
const fired = results.flatMap((r) => {
|
|
1151
1697
|
if (r === void 0) return [];
|
|
1152
|
-
if (
|
|
1698
|
+
if (r.receipt === true && late !== void 0) return [{ entries: r.entries ?? [] }];
|
|
1699
|
+
if (late === "input" || late === "wake" || late === "blocked" && r.continue === true) {
|
|
1153
1700
|
r.yielded?.();
|
|
1154
1701
|
return [];
|
|
1155
1702
|
}
|
|
@@ -1176,54 +1723,294 @@ function parallelSettle(pi, hold = noHold) {
|
|
|
1176
1723
|
function outcomeOf(event) {
|
|
1177
1724
|
return typeof event === "object" && event !== null ? event.outcome : void 0;
|
|
1178
1725
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1726
|
+
var SETTLE_DRAIN_MS = 3e3;
|
|
1727
|
+
var SETTLE_WAIT_ARGS = ["wait", "--first", "--on-stall", "exit", "--json"];
|
|
1728
|
+
var SETTLE_READY_ARGS = [
|
|
1729
|
+
"wait",
|
|
1730
|
+
"--first",
|
|
1731
|
+
"--on-stall",
|
|
1732
|
+
"exit",
|
|
1733
|
+
"--timeout",
|
|
1734
|
+
"1",
|
|
1735
|
+
"--json"
|
|
1736
|
+
];
|
|
1737
|
+
var READY_MAX = 50;
|
|
1738
|
+
var ESC = /^\u001b(\[27(;\d+)*u)?$/;
|
|
1739
|
+
function envelopeOf(stdout) {
|
|
1740
|
+
try {
|
|
1741
|
+
const v = JSON.parse(stdout);
|
|
1742
|
+
return typeof v === "object" && v !== null && typeof v.ok === "boolean" ? v : void 0;
|
|
1743
|
+
} catch {
|
|
1744
|
+
return void 0;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function sessionIdOf(ctx) {
|
|
1748
|
+
try {
|
|
1749
|
+
return ctx?.sessionManager?.getSessionId?.() || void 0;
|
|
1750
|
+
} catch {
|
|
1751
|
+
return void 0;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
function identityEnvs(sid) {
|
|
1755
|
+
const e = process.env;
|
|
1756
|
+
const out = [];
|
|
1757
|
+
if (e.MU_MANAGED_AGENT === "1" && e.MU_WORKSTREAM && e.MU_AGENT_NAME)
|
|
1758
|
+
out.push({ PI_SESSION_ID: void 0 });
|
|
1759
|
+
if (sid !== void 0) out.push({ MU_MANAGED_AGENT: void 0, PI_SESSION_ID: sid });
|
|
1760
|
+
return out;
|
|
1761
|
+
}
|
|
1762
|
+
async function humanNeeds(run, sid) {
|
|
1763
|
+
const counts = await Promise.all(
|
|
1764
|
+
identityEnvs(sid).map(async (env) => {
|
|
1765
|
+
const r = await run(["state", "--me", "--all", "--full", "--json"], void 0, void 0, {
|
|
1766
|
+
env
|
|
1767
|
+
}).catch(() => void 0);
|
|
1768
|
+
const env0 = r === void 0 ? void 0 : envelopeOf(r.stdout);
|
|
1769
|
+
const ws = env0?.ok === true ? env0.data?.workstreams : void 0;
|
|
1770
|
+
if (!Array.isArray(ws)) return void 0;
|
|
1771
|
+
return ws.reduce(
|
|
1772
|
+
(n, w) => n + (w.human_needs?.length ?? 0),
|
|
1773
|
+
0
|
|
1774
|
+
);
|
|
1775
|
+
})
|
|
1776
|
+
);
|
|
1777
|
+
return counts.some((c) => c === void 0) ? void 0 : counts.reduce((a, b) => a + (b ?? 0), 0);
|
|
1778
|
+
}
|
|
1779
|
+
function entry(content) {
|
|
1780
|
+
return { type: "custom_message", customType: NUDGE_MESSAGE_TYPE, content, display: true };
|
|
1781
|
+
}
|
|
1782
|
+
var REPORT_CAP = 4e3;
|
|
1783
|
+
var WAVE_REPORT_CAP = 1500;
|
|
1784
|
+
var PENDING_STATES = /* @__PURE__ */ new Set(["queued", "starting", "running", "recording"]);
|
|
1785
|
+
var recordOf = (c) => c.record?.workstream && c.record.task ? `${c.record.workstream}/${c.record.task}` : void 0;
|
|
1786
|
+
async function reportOf(mu, c) {
|
|
1787
|
+
const v = await mu(["task", "notes", c.task, "-w", c.workstream, "--json"]);
|
|
1788
|
+
const items = v?.ok === true ? v.data?.items : void 0;
|
|
1789
|
+
return items?.filter((n) => n.kind === "report").at(-1)?.content;
|
|
1790
|
+
}
|
|
1791
|
+
async function answerBlock(mu, c, cap) {
|
|
1792
|
+
const where = recordOf(c);
|
|
1793
|
+
const notes = `mu task notes ${c.task} -w ${c.workstream}`;
|
|
1794
|
+
const head = [
|
|
1795
|
+
`Delegate answer: call ${c.callId}, task ${c.workstream}/${c.task}, ${c.state}`,
|
|
1796
|
+
c.verdict ? `, ${c.verdict}` : "",
|
|
1797
|
+
where ? `, recorded on ${where}` : "",
|
|
1798
|
+
c.error ? `. Error: ${c.error}` : "",
|
|
1799
|
+
"."
|
|
1800
|
+
].join("");
|
|
1801
|
+
if (where && c.verdict) return `${head} Full answer: ${notes}`;
|
|
1802
|
+
const text = await reportOf(mu, c);
|
|
1803
|
+
if (text === void 0) return `${head} Report unreadable here; read it: ${notes}`;
|
|
1804
|
+
const body = text.length > cap ? `${text.slice(0, cap)}
|
|
1805
|
+
[... ${text.length - cap} more characters: ${notes}]` : text;
|
|
1806
|
+
return `${head}
|
|
1807
|
+
${body}`;
|
|
1808
|
+
}
|
|
1809
|
+
function waveSummary(wave, rows) {
|
|
1810
|
+
const n = (p) => rows.filter(p).length;
|
|
1811
|
+
const refused = rows.filter((c) => c.state === "refused");
|
|
1812
|
+
const counts = [
|
|
1813
|
+
`${n((c) => c.verdict === "CONFIRMED")}/${rows.length} CONFIRMED`,
|
|
1814
|
+
`${n((c) => c.verdict === "REFUTED")} REFUTED`,
|
|
1815
|
+
`${n((c) => c.state !== "refused" && !PENDING_STATES.has(c.state) && c.verdict !== "CONFIRMED" && c.verdict !== "REFUTED")} UNVERIFIED`,
|
|
1816
|
+
`${n((c) => PENDING_STATES.has(c.state))} pending`,
|
|
1817
|
+
`${refused.length} refused`
|
|
1818
|
+
].join(", ");
|
|
1819
|
+
const lines = rows.map(
|
|
1820
|
+
(c) => `- ${recordOf(c) ?? `${c.workstream}/${c.task}`}: ${c.state === "refused" ? "REFUSED, reissue it: mu_delegate_reissue" : c.verdict ?? c.state} (call ${c.callId})`
|
|
1821
|
+
);
|
|
1822
|
+
return [`Wave ${wave}: ${counts}.`, ...lines].join("\n");
|
|
1823
|
+
}
|
|
1824
|
+
function firingDelegate(d) {
|
|
1825
|
+
const f = d.firing;
|
|
1826
|
+
if (f?.kind !== "delegate" || typeof f.ref !== "string") return void 0;
|
|
1827
|
+
return f.ref.startsWith("delegate:") ? f.ref.slice("delegate:".length) : void 0;
|
|
1828
|
+
}
|
|
1829
|
+
function pictureOf(d) {
|
|
1830
|
+
if (typeof d.snapshotsCut === "number" && d.snapshotsCut > 0) return void 0;
|
|
1831
|
+
const snaps = Array.isArray(d.snapshots) ? d.snapshots : [];
|
|
1832
|
+
return new Set(
|
|
1833
|
+
snaps.flatMap(
|
|
1834
|
+
(s) => typeof s.ref === "string" && s.ref.startsWith("delegate:") ? [s.ref.slice("delegate:".length)] : []
|
|
1835
|
+
)
|
|
1836
|
+
);
|
|
1183
1837
|
}
|
|
1184
|
-
|
|
1838
|
+
var noDelivered = () => {
|
|
1839
|
+
};
|
|
1840
|
+
function registerNudge(pi, run = defaultRunner(), skillMd = readSkill(), nested = notNested, hold = noHold, delivered = noDelivered) {
|
|
1185
1841
|
if (!nudgeEnabled()) return;
|
|
1842
|
+
if (process.env.MU_REMOTE === "1") return;
|
|
1186
1843
|
const rule = skillMd === void 0 ? void 0 : keepDrivingRule(skillMd);
|
|
1187
1844
|
if (rule === void 0) return;
|
|
1188
|
-
|
|
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
|
-
});
|
|
1845
|
+
let current;
|
|
1846
|
+
const cancel = () => current?.abort();
|
|
1847
|
+
pi.on("input", cancel);
|
|
1848
|
+
pi.on("ui_prompt_start", cancel);
|
|
1849
|
+
pi.on("session_shutdown", cancel);
|
|
1199
1850
|
pi.on("agent_before_settle", async (event, ctx) => {
|
|
1200
|
-
if (
|
|
1851
|
+
if (nested() || hold(ctx)) return;
|
|
1201
1852
|
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
|
-
|
|
1853
|
+
const sid = sessionIdOf(ctx);
|
|
1854
|
+
if (identityEnvs(sid).length === 0) return;
|
|
1855
|
+
const env = { PI_SESSION_ID: sid };
|
|
1856
|
+
const ctl = new AbortController();
|
|
1857
|
+
current = ctl;
|
|
1858
|
+
const off = ctx?.ui?.onTerminalInput?.((data) => {
|
|
1859
|
+
if (ESC.test(data)) ctl.abort();
|
|
1860
|
+
return void 0;
|
|
1861
|
+
}) ?? (() => {
|
|
1862
|
+
});
|
|
1863
|
+
const wait = (args) => run(args, ctl.signal, void 0, { env, drainMs: SETTLE_DRAIN_MS });
|
|
1864
|
+
const mu = async (args) => envelopeOf(
|
|
1865
|
+
(await run(args, void 0, void 0, { env }).catch(() => void 0))?.stdout ?? ""
|
|
1866
|
+
);
|
|
1867
|
+
const call2 = async (id) => {
|
|
1868
|
+
const v = await mu(["delegates", "show", id, "--json"]);
|
|
1869
|
+
return v?.ok === true ? v.data?.call : void 0;
|
|
1870
|
+
};
|
|
1871
|
+
const answers = async (callId, shown, first) => {
|
|
1872
|
+
shown.add(callId);
|
|
1873
|
+
const c = await call2(callId);
|
|
1874
|
+
if (c === void 0)
|
|
1875
|
+
return {
|
|
1876
|
+
blocks: [
|
|
1877
|
+
`Delegate answer: call ${callId}. Its row is unreadable here: mu delegates show ${callId}`
|
|
1878
|
+
],
|
|
1879
|
+
stop: false
|
|
1880
|
+
};
|
|
1881
|
+
const wave = c.wave;
|
|
1882
|
+
if (!wave) return { blocks: [await answerBlock(mu, c, REPORT_CAP)], stop: false };
|
|
1883
|
+
const listed = async () => {
|
|
1884
|
+
const v = await mu(["delegates", "list", "--wave", wave, "--json"]);
|
|
1885
|
+
const items = v?.ok === true ? v.data?.items : void 0;
|
|
1886
|
+
return Array.isArray(items) ? items : [c];
|
|
1887
|
+
};
|
|
1888
|
+
const after = [];
|
|
1889
|
+
let stop = false;
|
|
1890
|
+
let rows = await listed();
|
|
1891
|
+
let picture = pictureOf(first);
|
|
1892
|
+
const owedRow = (r) => PENDING_STATES.has(r.state) || r.state !== "refused" && !shown.has(r.callId) && picture?.has(r.callId) !== false;
|
|
1893
|
+
while (!ctl.signal.aborted && rows.some(owedRow)) {
|
|
1894
|
+
const r = await wait(SETTLE_WAIT_ARGS);
|
|
1895
|
+
const v = envelopeOf(r.stdout);
|
|
1896
|
+
if (v?.ok === false && v.error === "WaitNothingError") break;
|
|
1897
|
+
const d = v?.ok === true ? v.data : void 0;
|
|
1898
|
+
if (d === void 0) {
|
|
1899
|
+
after.push(
|
|
1900
|
+
`The wave's wait failed (exit ${r.code}): ${(r.stdout.trim() || r.stderr.trim()).slice(0, 500)}`
|
|
1901
|
+
);
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
if (d.interrupted === true || d.drained === true) break;
|
|
1905
|
+
picture = pictureOf(d);
|
|
1906
|
+
if (d.timedOut === true) continue;
|
|
1907
|
+
if (Array.isArray(d.humanNeeds) && d.humanNeeds.length > 0) stop = true;
|
|
1908
|
+
const id = firingDelegate(d);
|
|
1909
|
+
const sibling = id !== void 0 && rows.some((x) => x.callId === id);
|
|
1910
|
+
if (id !== void 0) shown.add(id);
|
|
1911
|
+
if (sibling && !stop) {
|
|
1912
|
+
rows = await listed();
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
after.push(`Another event came before the rest of wave ${wave}:
|
|
1916
|
+
${JSON.stringify(d)}`);
|
|
1917
|
+
const oc = id === void 0 || sibling ? void 0 : await call2(id);
|
|
1918
|
+
if (oc) after.push(await answerBlock(mu, oc, REPORT_CAP));
|
|
1919
|
+
break;
|
|
1920
|
+
}
|
|
1921
|
+
rows = await listed();
|
|
1922
|
+
const bodies = await Promise.all(
|
|
1923
|
+
rows.filter((x) => shown.has(x.callId)).map((x) => answerBlock(mu, x, WAVE_REPORT_CAP))
|
|
1924
|
+
);
|
|
1925
|
+
return { blocks: [waveSummary(wave, rows), ...bodies, ...after], stop };
|
|
1926
|
+
};
|
|
1927
|
+
const ready = async () => {
|
|
1928
|
+
const blocks = [];
|
|
1929
|
+
const shown = [];
|
|
1930
|
+
for (let i = 0; i < READY_MAX && !ctl.signal.aborted; i++) {
|
|
1931
|
+
const started = Date.now();
|
|
1932
|
+
const r = await wait(SETTLE_READY_ARGS);
|
|
1933
|
+
const v = envelopeOf(r.stdout);
|
|
1934
|
+
if (v?.ok === false && v.error === "WaitNothingError") {
|
|
1935
|
+
delivered({ before: started });
|
|
1936
|
+
break;
|
|
1937
|
+
}
|
|
1938
|
+
const d = v?.ok === true ? v.data : void 0;
|
|
1939
|
+
if (d === void 0) {
|
|
1940
|
+
const why = (r.stdout.trim() || r.stderr.trim()).slice(0, 2e3);
|
|
1941
|
+
blocks.push(`mu wait failed (exit ${r.code}): ${why}`);
|
|
1942
|
+
break;
|
|
1943
|
+
}
|
|
1944
|
+
if (d.drained === true) delivered({ before: started });
|
|
1945
|
+
if (d.interrupted === true || d.drained === true || d.timedOut === true) break;
|
|
1946
|
+
blocks.push(JSON.stringify(d));
|
|
1947
|
+
const id = firingDelegate(d);
|
|
1948
|
+
if (id === void 0) continue;
|
|
1949
|
+
shown.push(id);
|
|
1950
|
+
const c = await call2(id);
|
|
1951
|
+
blocks.push(
|
|
1952
|
+
c === void 0 ? `Delegate answer: call ${id}. Its row is unreadable here: mu delegates show ${id}` : await answerBlock(mu, c, REPORT_CAP)
|
|
1953
|
+
);
|
|
1225
1954
|
}
|
|
1955
|
+
if (shown.length > 0) delivered(shown);
|
|
1956
|
+
if (blocks.length === 0) return void 0;
|
|
1957
|
+
const text = [
|
|
1958
|
+
"[mu] A human stop is open (mu human-needed), so the settle did not wait. It showed what was already ready:",
|
|
1959
|
+
...blocks.flatMap((b) => ["", b])
|
|
1960
|
+
].join("\n");
|
|
1961
|
+
return { entries: [entry(text)], receipt: true };
|
|
1226
1962
|
};
|
|
1963
|
+
let result;
|
|
1964
|
+
try {
|
|
1965
|
+
result = await (async () => {
|
|
1966
|
+
for (; ; ) {
|
|
1967
|
+
const needs = await humanNeeds(run, sid);
|
|
1968
|
+
if (ctl.signal.aborted || hold(ctx)) return void 0;
|
|
1969
|
+
if (needs === void 0)
|
|
1970
|
+
return {
|
|
1971
|
+
entries: [
|
|
1972
|
+
entry(
|
|
1973
|
+
"[mu] settle: could not read your open human needs (mu state --me); not waiting."
|
|
1974
|
+
)
|
|
1975
|
+
]
|
|
1976
|
+
};
|
|
1977
|
+
if (needs > 0) return ready();
|
|
1978
|
+
const started = Date.now();
|
|
1979
|
+
const r = await wait(SETTLE_WAIT_ARGS);
|
|
1980
|
+
const v = envelopeOf(r.stdout);
|
|
1981
|
+
if (v?.ok === false && v.error === "WaitNothingError") {
|
|
1982
|
+
delivered({ before: started });
|
|
1983
|
+
return void 0;
|
|
1984
|
+
}
|
|
1985
|
+
if (v === void 0 || v.ok !== true || v.data === void 0) {
|
|
1986
|
+
const why = (r.stdout.trim() || r.stderr.trim()).slice(0, 2e3);
|
|
1987
|
+
return { entries: [entry(`[mu] settle: mu wait failed (exit ${r.code}): ${why}`)] };
|
|
1988
|
+
}
|
|
1989
|
+
const d = v.data;
|
|
1990
|
+
if (d.drained === true) delivered({ before: started });
|
|
1991
|
+
if (d.interrupted === true || d.drained === true) return void 0;
|
|
1992
|
+
if (d.timedOut === true) continue;
|
|
1993
|
+
const callId = firingDelegate(d);
|
|
1994
|
+
const shown = /* @__PURE__ */ new Set();
|
|
1995
|
+
const got = callId === void 0 ? void 0 : await answers(callId, shown, d);
|
|
1996
|
+
const blocks = got?.blocks ?? [];
|
|
1997
|
+
if (shown.size > 0) delivered([...shown]);
|
|
1998
|
+
const stop = Array.isArray(d.humanNeeds) && d.humanNeeds.length > 0 || got?.stop === true;
|
|
1999
|
+
const text = [
|
|
2000
|
+
`[mu] ${rule}`,
|
|
2001
|
+
"",
|
|
2002
|
+
`Work you own is still running, so the settle ran \`mu ${SETTLE_WAIT_ARGS.join(" ")}\` for you. Its result:`,
|
|
2003
|
+
JSON.stringify(d),
|
|
2004
|
+
...blocks.flatMap((b) => ["", b])
|
|
2005
|
+
].join("\n");
|
|
2006
|
+
return { entries: [entry(text)], ...stop ? {} : { continue: true }, receipt: true };
|
|
2007
|
+
}
|
|
2008
|
+
})();
|
|
2009
|
+
} finally {
|
|
2010
|
+
off();
|
|
2011
|
+
if (current === ctl) current = void 0;
|
|
2012
|
+
}
|
|
2013
|
+
return result;
|
|
1227
2014
|
});
|
|
1228
2015
|
}
|
|
1229
2016
|
function workerIdentity(env = process.env) {
|
|
@@ -1233,12 +2020,15 @@ function workerIdentity(env = process.env) {
|
|
|
1233
2020
|
return { agent, workstream };
|
|
1234
2021
|
}
|
|
1235
2022
|
async function ownedInProgress(run, agent, workstream) {
|
|
1236
|
-
const r = await run(["task", "owned-by", agent, "-w", workstream, "--json"]);
|
|
1237
|
-
if (r.code !== 0) return void 0;
|
|
1238
2023
|
try {
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
|
|
2024
|
+
const items = await allItems(
|
|
2025
|
+
run,
|
|
2026
|
+
["task", "owned-by", agent, "-w", workstream, "--json"],
|
|
2027
|
+
"--full"
|
|
2028
|
+
);
|
|
2029
|
+
if (items === void 0) return void 0;
|
|
2030
|
+
return items.flatMap(
|
|
2031
|
+
(t) => t.status === "IN_PROGRESS" && t.kind !== "delegate" && t.id ? [t.id] : []
|
|
1242
2032
|
);
|
|
1243
2033
|
} catch {
|
|
1244
2034
|
return void 0;
|
|
@@ -1255,6 +2045,7 @@ function closeNudgeText(workstream, names) {
|
|
|
1255
2045
|
}
|
|
1256
2046
|
function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested = notNested, hold = noHold) {
|
|
1257
2047
|
if (!nudgeEnabled(env)) return;
|
|
2048
|
+
if (env.MU_REMOTE === "1") return;
|
|
1258
2049
|
const me = workerIdentity(env);
|
|
1259
2050
|
if (me === void 0) return;
|
|
1260
2051
|
let fired = false;
|
|
@@ -1295,74 +2086,9 @@ function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested
|
|
|
1295
2086
|
};
|
|
1296
2087
|
});
|
|
1297
2088
|
}
|
|
1298
|
-
var REFUTE_MARK = /^(REFUTER |VERDICT:|REFUTE-EXEMPT:)/m;
|
|
1299
|
-
async function briefRefuted(run, ws, id) {
|
|
1300
|
-
const r = await run(["task", "notes", id, ...ws ? ["-w", ws] : [], "--json"]);
|
|
1301
|
-
if (r.code !== 0) return void 0;
|
|
1302
|
-
try {
|
|
1303
|
-
const json = JSON.parse(r.stdout);
|
|
1304
|
-
return (json.items ?? []).some((n) => REFUTE_MARK.test(n.content ?? ""));
|
|
1305
|
-
} catch {
|
|
1306
|
-
return void 0;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
function refuteNudgeText(refs) {
|
|
1310
|
-
return `[mu] dispatched ${refs.join(", ")} with an unrefuted brief: refute it (delegate, record) or note REFUTE-EXEMPT: <why>`;
|
|
1311
|
-
}
|
|
1312
|
-
function registerRefuteNudge(pi, run = defaultRunner(), nested = notNested, hold = noHold) {
|
|
1313
|
-
if (!nudgeEnabled()) return;
|
|
1314
|
-
const answerDue = (ctx) => hold(ctx) === "answer";
|
|
1315
|
-
const armed = /* @__PURE__ */ new Map();
|
|
1316
|
-
let fired = false;
|
|
1317
|
-
pi.on("input", () => {
|
|
1318
|
-
armed.clear();
|
|
1319
|
-
fired = false;
|
|
1320
|
-
});
|
|
1321
|
-
pi.on("tool_call", (event) => {
|
|
1322
|
-
const cmd = bashCommand(event);
|
|
1323
|
-
if (cmd === void 0) return;
|
|
1324
|
-
for (const t of dispatchedTasks(cmd)) {
|
|
1325
|
-
if (!t.id.startsWith("review_")) armed.set(`${t.ws}/${t.id}`, t);
|
|
1326
|
-
}
|
|
1327
|
-
});
|
|
1328
|
-
pi.on("agent_before_settle", async (event, ctx) => {
|
|
1329
|
-
if (fired || armed.size === 0 || nested() || noUI(ctx) || answerDue(ctx)) return;
|
|
1330
|
-
if (outcomeOf(event) !== "completed") return;
|
|
1331
|
-
fired = true;
|
|
1332
|
-
const tasks = [...armed];
|
|
1333
|
-
const refuted = await Promise.all(tasks.map(([, t]) => briefRefuted(run, t.ws, t.id)));
|
|
1334
|
-
const refs = tasks.flatMap(([ref], i) => refuted[i] === false ? [ref] : []);
|
|
1335
|
-
if (refs.length === 0) return;
|
|
1336
|
-
if (answerDue(ctx)) {
|
|
1337
|
-
fired = false;
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
const logWs = [...armed.values()].find((t) => t.ws !== "")?.ws;
|
|
1341
|
-
breadcrumb(run, [
|
|
1342
|
-
"log",
|
|
1343
|
-
...logWs ? ["-w", logWs] : [],
|
|
1344
|
-
"--kind",
|
|
1345
|
-
NUDGE_LOG_KIND,
|
|
1346
|
-
`refute: ${refs.join(", ")}`
|
|
1347
|
-
]);
|
|
1348
|
-
return {
|
|
1349
|
-
entries: [
|
|
1350
|
-
{
|
|
1351
|
-
type: "custom_message",
|
|
1352
|
-
customType: REFUTE_NUDGE_MESSAGE_TYPE,
|
|
1353
|
-
content: refuteNudgeText(refs),
|
|
1354
|
-
display: true
|
|
1355
|
-
}
|
|
1356
|
-
],
|
|
1357
|
-
yielded: () => {
|
|
1358
|
-
fired = false;
|
|
1359
|
-
}
|
|
1360
|
-
};
|
|
1361
|
-
});
|
|
1362
|
-
}
|
|
1363
2089
|
|
|
1364
2090
|
// extension/mu-pi.ts
|
|
1365
|
-
var EXT_VERSION = true ? "
|
|
2091
|
+
var EXT_VERSION = true ? "4.0.0" : void 0;
|
|
1366
2092
|
var FRESH_COMMAND = "mu-fresh";
|
|
1367
2093
|
var FRESH_TIMEOUT_MS = 3e4;
|
|
1368
2094
|
function commandName(name) {
|
|
@@ -1378,9 +2104,9 @@ function fail(error) {
|
|
|
1378
2104
|
}
|
|
1379
2105
|
function detectPiVersion() {
|
|
1380
2106
|
try {
|
|
1381
|
-
const
|
|
1382
|
-
if (!
|
|
1383
|
-
let dir = dirname(realpathSync(
|
|
2107
|
+
const entry2 = process.argv[1];
|
|
2108
|
+
if (!entry2) return void 0;
|
|
2109
|
+
let dir = dirname(realpathSync(entry2));
|
|
1384
2110
|
for (let i = 0; i < 6; i++) {
|
|
1385
2111
|
const pkgPath = join4(dir, "package.json");
|
|
1386
2112
|
if (existsSync3(pkgPath)) {
|
|
@@ -1416,22 +2142,22 @@ function sharedMap() {
|
|
|
1416
2142
|
}
|
|
1417
2143
|
function lastAssistantText(event) {
|
|
1418
2144
|
const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
|
|
1419
|
-
if (!Array.isArray(msgs)) return "";
|
|
2145
|
+
if (!Array.isArray(msgs)) return { text: "", truncated: false };
|
|
1420
2146
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1421
2147
|
const m = msgs[i];
|
|
1422
2148
|
if (typeof m !== "object" || m === null) continue;
|
|
1423
2149
|
const { role, content } = m;
|
|
1424
2150
|
if (role !== "assistant") continue;
|
|
1425
|
-
if (typeof content === "string") return
|
|
1426
|
-
if (!Array.isArray(content)) return "";
|
|
2151
|
+
if (typeof content === "string") return capTail(content);
|
|
2152
|
+
if (!Array.isArray(content)) return { text: "", truncated: false };
|
|
1427
2153
|
const parts = content.flatMap((c) => {
|
|
1428
2154
|
if (typeof c !== "object" || c === null) return [];
|
|
1429
2155
|
const { type, text } = c;
|
|
1430
2156
|
return type === "text" && typeof text === "string" ? [text] : [];
|
|
1431
2157
|
});
|
|
1432
|
-
return
|
|
2158
|
+
return capTail(parts.join(""));
|
|
1433
2159
|
}
|
|
1434
|
-
return "";
|
|
2160
|
+
return { text: "", truncated: false };
|
|
1435
2161
|
}
|
|
1436
2162
|
function lastAssistantError(event) {
|
|
1437
2163
|
const msgs = typeof event === "object" && event !== null ? event.messages : void 0;
|
|
@@ -1446,6 +2172,13 @@ function lastAssistantError(event) {
|
|
|
1446
2172
|
}
|
|
1447
2173
|
return void 0;
|
|
1448
2174
|
}
|
|
2175
|
+
function capTail(text) {
|
|
2176
|
+
if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return { text, truncated: false };
|
|
2177
|
+
const buf = Buffer.from(text, "utf8");
|
|
2178
|
+
const tail = buf.subarray(buf.length - LAST_TEXT_MAX_BYTES).toString("utf8").replace(/^\uFFFD/, "");
|
|
2179
|
+
return { text: `${LAST_TEXT_TRUNCATED}
|
|
2180
|
+
${tail}`, truncated: true };
|
|
2181
|
+
}
|
|
1449
2182
|
function capText(text) {
|
|
1450
2183
|
if (Buffer.byteLength(text, "utf8") <= LAST_TEXT_MAX_BYTES) return text;
|
|
1451
2184
|
const cut = Buffer.from(text, "utf8").subarray(0, LAST_TEXT_MAX_BYTES).toString("utf8");
|
|
@@ -1524,38 +2257,15 @@ function reasonOf(event) {
|
|
|
1524
2257
|
const r = typeof event === "object" && event !== null ? event.reason : void 0;
|
|
1525
2258
|
return typeof r === "string" ? r : void 0;
|
|
1526
2259
|
}
|
|
1527
|
-
function promptsDir(from = import.meta.url) {
|
|
1528
|
-
let here;
|
|
1529
|
-
try {
|
|
1530
|
-
here = dirname(fileURLToPath2(from));
|
|
1531
|
-
} catch {
|
|
1532
|
-
return void 0;
|
|
1533
|
-
}
|
|
1534
|
-
for (const up of ["..", "../.."]) {
|
|
1535
|
-
const d = join4(here, up, "prompts");
|
|
1536
|
-
if (existsSync3(join4(d, "ultrathink.md"))) return d;
|
|
1537
|
-
}
|
|
1538
|
-
return void 0;
|
|
1539
|
-
}
|
|
1540
|
-
function registerPrompts(pi) {
|
|
1541
|
-
pi.on("resources_discover", () => {
|
|
1542
|
-
const d = promptsDir();
|
|
1543
|
-
if (!d) return {};
|
|
1544
|
-
const paths = readdirSync(d).filter((f) => f.endsWith(".md")).sort().map((f) => join4(d, f));
|
|
1545
|
-
return { promptPaths: paths };
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
2260
|
function muPi(pi, run = defaultRunner()) {
|
|
1549
2261
|
const delegates = registerDelegate(pi, run);
|
|
1550
|
-
registerPrompts(pi);
|
|
1551
2262
|
const nested = serveCtl(pi);
|
|
1552
2263
|
const hold = withQueuedInput(
|
|
1553
|
-
() => delegates.answerPending() ? "
|
|
2264
|
+
() => delegates.answerPending() ? "wake" : isUiBlocked() ? "blocked" : void 0
|
|
1554
2265
|
);
|
|
1555
2266
|
const settle = parallelSettle(pi, hold);
|
|
1556
|
-
registerNudge(settle.api, run, void 0, nested, hold);
|
|
2267
|
+
registerNudge(settle.api, run, void 0, nested, hold, delegates.delivered);
|
|
1557
2268
|
registerCloseNudge(settle.api, run, process.env, nested, hold);
|
|
1558
|
-
registerRefuteNudge(settle.api, run, nested, hold);
|
|
1559
2269
|
settle.flush();
|
|
1560
2270
|
}
|
|
1561
2271
|
function serveCtl(pi) {
|
|
@@ -1570,25 +2280,42 @@ function serveCtl(pi) {
|
|
|
1570
2280
|
const existing = map.get(sockPath);
|
|
1571
2281
|
const g = existing ?? {
|
|
1572
2282
|
conns: /* @__PURE__ */ new Set(),
|
|
2283
|
+
incarnation: randomUUID(),
|
|
2284
|
+
accepted: /* @__PURE__ */ new Map(),
|
|
1573
2285
|
waiters: /* @__PURE__ */ new Set(),
|
|
1574
2286
|
state: "idle",
|
|
1575
2287
|
since: Date.now(),
|
|
1576
2288
|
runs: 0,
|
|
2289
|
+
tools: /* @__PURE__ */ new Map(),
|
|
1577
2290
|
lastText: "",
|
|
1578
2291
|
pi,
|
|
1579
2292
|
handle
|
|
1580
2293
|
};
|
|
1581
2294
|
g.pi = pi;
|
|
1582
2295
|
g.handle = handle;
|
|
2296
|
+
g.incarnation ??= randomUUID();
|
|
2297
|
+
g.accepted ??= /* @__PURE__ */ new Map();
|
|
2298
|
+
g.tools ??= /* @__PURE__ */ new Map();
|
|
1583
2299
|
map.set(sockPath, g);
|
|
1584
2300
|
const status = () => ({
|
|
2301
|
+
incarnation: g.incarnation,
|
|
1585
2302
|
state: g.state,
|
|
1586
2303
|
since: g.since,
|
|
1587
2304
|
runs: g.runs,
|
|
1588
2305
|
pending: g.ctx?.hasPendingMessages() ?? false,
|
|
2306
|
+
tool: [...g.tools.values()].at(-1) ?? null,
|
|
1589
2307
|
...sessionFileOf(g.ctx),
|
|
1590
2308
|
...blockedField()
|
|
1591
2309
|
});
|
|
2310
|
+
const remember = (id, runs) => {
|
|
2311
|
+
if (id === void 0) return;
|
|
2312
|
+
g.accepted.delete(id);
|
|
2313
|
+
g.accepted.set(id, { runs, at: Date.now() });
|
|
2314
|
+
for (const old of g.accepted.keys()) {
|
|
2315
|
+
if (g.accepted.size <= CTL_MSGID_MEMORY) break;
|
|
2316
|
+
g.accepted.delete(old);
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
1592
2319
|
const blockedField = () => {
|
|
1593
2320
|
const b = uiBlocked();
|
|
1594
2321
|
return b ? { blocked: b } : {};
|
|
@@ -1614,8 +2341,20 @@ function serveCtl(pi) {
|
|
|
1614
2341
|
g.since = Date.now();
|
|
1615
2342
|
if (g.fresh?.armed) settleFresh({ v: V, ok: true, ...status() });
|
|
1616
2343
|
});
|
|
2344
|
+
pi.on("tool_execution_start", (e) => {
|
|
2345
|
+
const { toolCallId, toolName } = e ?? {};
|
|
2346
|
+
if (typeof toolCallId === "string" && typeof toolName === "string") {
|
|
2347
|
+
g.tools.set(toolCallId, { name: toolName, since: Date.now() });
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
pi.on("tool_execution_end", (e) => {
|
|
2351
|
+
g.tools.delete(String(e?.toolCallId));
|
|
2352
|
+
});
|
|
1617
2353
|
pi.on("agent_end", (e) => {
|
|
1618
|
-
g.
|
|
2354
|
+
g.tools.clear();
|
|
2355
|
+
const cut = lastAssistantText(e);
|
|
2356
|
+
g.endText = cut.text;
|
|
2357
|
+
g.endTextTruncated = cut.truncated;
|
|
1619
2358
|
g.endError = lastAssistantError(e);
|
|
1620
2359
|
});
|
|
1621
2360
|
function sessionFileOf(c) {
|
|
@@ -1630,9 +2369,12 @@ function serveCtl(pi) {
|
|
|
1630
2369
|
g.ctx = c;
|
|
1631
2370
|
g.state = "idle";
|
|
1632
2371
|
g.since = Date.now();
|
|
2372
|
+
g.tools.clear();
|
|
1633
2373
|
g.runs++;
|
|
1634
2374
|
g.lastText = g.endText ?? "";
|
|
2375
|
+
g.lastTextTruncated = g.endTextTruncated === true;
|
|
1635
2376
|
g.endText = void 0;
|
|
2377
|
+
g.endTextTruncated = void 0;
|
|
1636
2378
|
g.lastError = g.endError;
|
|
1637
2379
|
g.endError = void 0;
|
|
1638
2380
|
g.sessionFile = sessionFileOf(c).sessionFile;
|
|
@@ -1693,6 +2435,8 @@ function serveCtl(pi) {
|
|
|
1693
2435
|
});
|
|
1694
2436
|
}
|
|
1695
2437
|
function command(req) {
|
|
2438
|
+
const stale = incarnationRefusal(req);
|
|
2439
|
+
if (stale) return stale;
|
|
1696
2440
|
const name = CTL_COMMANDS.find((n) => n === req.name);
|
|
1697
2441
|
if (name === void 0) {
|
|
1698
2442
|
return fail(`command must be one of ${CTL_COMMANDS.join(", ")}`);
|
|
@@ -1718,8 +2462,12 @@ function serveCtl(pi) {
|
|
|
1718
2462
|
});
|
|
1719
2463
|
}
|
|
1720
2464
|
function fresh(req) {
|
|
2465
|
+
const stale = incarnationRefusal(req, "expectIncarnation");
|
|
2466
|
+
if (stale) return stale;
|
|
1721
2467
|
const text = str(req, "text");
|
|
1722
2468
|
if (text === void 0) return fail("fresh needs a string text");
|
|
2469
|
+
const id = msgIdOf(req);
|
|
2470
|
+
if (id.error) return fail(id.error);
|
|
1723
2471
|
if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
|
|
1724
2472
|
const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
|
|
1725
2473
|
if (busy && req.force !== true) return fail("busy");
|
|
@@ -1730,6 +2478,7 @@ function serveCtl(pi) {
|
|
|
1730
2478
|
armed: false,
|
|
1731
2479
|
settle: (r) => {
|
|
1732
2480
|
clearTimeout(timer);
|
|
2481
|
+
if (r.ok) remember(id.id, r.runs ?? g.runs);
|
|
1733
2482
|
resolve(r);
|
|
1734
2483
|
}
|
|
1735
2484
|
};
|
|
@@ -1738,6 +2487,17 @@ function serveCtl(pi) {
|
|
|
1738
2487
|
).catch((e) => settleFresh(fail(e instanceof Error ? e.message : String(e))));
|
|
1739
2488
|
});
|
|
1740
2489
|
}
|
|
2490
|
+
function incarnationRefusal(req, key = "expectIncarnation") {
|
|
2491
|
+
if (!(key in req) || req[key] === g.incarnation) return void 0;
|
|
2492
|
+
return { ...fail(INCARNATION_MISMATCH), incarnation: g.incarnation };
|
|
2493
|
+
}
|
|
2494
|
+
function msgIdOf(req) {
|
|
2495
|
+
if (!("msgId" in req)) return {};
|
|
2496
|
+
const id = req.msgId;
|
|
2497
|
+
if (typeof id === "string" && id.length >= 1 && id.length <= CTL_MSGID_MAX_LENGTH)
|
|
2498
|
+
return { id };
|
|
2499
|
+
return { error: `msgId must be a string of 1-${CTL_MSGID_MAX_LENGTH} characters` };
|
|
2500
|
+
}
|
|
1741
2501
|
async function handle(req, conn) {
|
|
1742
2502
|
switch (req.op) {
|
|
1743
2503
|
case "hello":
|
|
@@ -1747,18 +2507,31 @@ function serveCtl(pi) {
|
|
|
1747
2507
|
agent: process.env.MU_AGENT_NAME,
|
|
1748
2508
|
workstream: process.env.MU_WORKSTREAM,
|
|
1749
2509
|
piVersion,
|
|
2510
|
+
incarnation: g.incarnation,
|
|
1750
2511
|
ops: [...CTL_OPS],
|
|
2512
|
+
features: [...CTL_FEATURES],
|
|
1751
2513
|
...EXT_VERSION !== void 0 ? { extVersion: EXT_VERSION } : {}
|
|
1752
2514
|
};
|
|
1753
|
-
case "status":
|
|
1754
|
-
|
|
2515
|
+
case "status": {
|
|
2516
|
+
const id = msgIdOf(req);
|
|
2517
|
+
if (id.error) return fail(id.error);
|
|
2518
|
+
if (id.id === void 0) return { v: V, ok: true, ...status() };
|
|
2519
|
+
const got = g.accepted.get(id.id);
|
|
2520
|
+
const msg = got ? { id: id.id, accepted: true, ...got } : { id: id.id, accepted: false };
|
|
2521
|
+
return { v: V, ok: true, ...status(), msg };
|
|
2522
|
+
}
|
|
1755
2523
|
case "send": {
|
|
2524
|
+
const stale = incarnationRefusal(req);
|
|
2525
|
+
if (stale) return stale;
|
|
1756
2526
|
const text = str(req, "text");
|
|
1757
2527
|
if (text === void 0) return fail("send needs a string text");
|
|
2528
|
+
const id = msgIdOf(req);
|
|
2529
|
+
if (id.error) return fail(id.error);
|
|
1758
2530
|
const mode = req.mode === "steer" ? "steer" : "followUp";
|
|
1759
2531
|
const before = status();
|
|
1760
2532
|
if (g.ctx?.isIdle() ?? g.state === "idle") await g.pi.sendUserMessage(text);
|
|
1761
2533
|
else await g.pi.sendUserMessage(text, { deliverAs: mode });
|
|
2534
|
+
remember(id.id, before.runs);
|
|
1762
2535
|
return { v: V, ok: true, ...before };
|
|
1763
2536
|
}
|
|
1764
2537
|
case "fresh":
|
|
@@ -1766,6 +2539,9 @@ function serveCtl(pi) {
|
|
|
1766
2539
|
case "command":
|
|
1767
2540
|
return command(req);
|
|
1768
2541
|
case "wait":
|
|
2542
|
+
if ("incarnation" in req && req.incarnation !== g.incarnation) {
|
|
2543
|
+
return { ...fail(INCARNATION_MISMATCH), incarnation: g.incarnation };
|
|
2544
|
+
}
|
|
1769
2545
|
return wait(
|
|
1770
2546
|
num(req, "afterRuns"),
|
|
1771
2547
|
num(req, "timeoutMs"),
|
|
@@ -1786,8 +2562,12 @@ function serveCtl(pi) {
|
|
|
1786
2562
|
}
|
|
1787
2563
|
}
|
|
1788
2564
|
async function interrupt(req, conn) {
|
|
2565
|
+
const stale = incarnationRefusal(req);
|
|
2566
|
+
if (stale) return stale;
|
|
1789
2567
|
const text = str(req, "text");
|
|
1790
2568
|
if (text === void 0) return fail("interrupt needs a string text");
|
|
2569
|
+
const id = msgIdOf(req);
|
|
2570
|
+
if (id.error) return fail(id.error);
|
|
1791
2571
|
const wasBusy = !(g.ctx?.isIdle() ?? g.state === "idle");
|
|
1792
2572
|
const { pending } = status();
|
|
1793
2573
|
if (wasBusy) {
|
|
@@ -1799,10 +2579,12 @@ function serveCtl(pi) {
|
|
|
1799
2579
|
}
|
|
1800
2580
|
const before = status();
|
|
1801
2581
|
await g.pi.sendUserMessage(text);
|
|
2582
|
+
remember(id.id, before.runs);
|
|
1802
2583
|
return { v: V, ok: true, ...before, pending, wasBusy };
|
|
1803
2584
|
}
|
|
1804
2585
|
const settledRun = () => ({
|
|
1805
2586
|
lastText: g.lastText,
|
|
2587
|
+
...g.lastTextTruncated === true ? { lastTextTruncated: true } : {},
|
|
1806
2588
|
...g.lastError !== void 0 ? { lastError: g.lastError } : {},
|
|
1807
2589
|
...g.sessionFile !== void 0 ? { sessionFile: g.sessionFile } : {}
|
|
1808
2590
|
});
|
|
@@ -1926,8 +2708,8 @@ function serveCtl(pi) {
|
|
|
1926
2708
|
await bind();
|
|
1927
2709
|
});
|
|
1928
2710
|
pi.on("session_shutdown", async (e) => {
|
|
1929
|
-
const
|
|
1930
|
-
if (
|
|
2711
|
+
const reason2 = reasonOf(e);
|
|
2712
|
+
if (reason2 === "new" || reason2 === "resume" || reason2 === "fork" || reason2 === "reload") {
|
|
1931
2713
|
return;
|
|
1932
2714
|
}
|
|
1933
2715
|
const s = g.server;
|
|
@@ -1957,7 +2739,6 @@ export {
|
|
|
1957
2739
|
isUiBlocked,
|
|
1958
2740
|
lastAssistantError,
|
|
1959
2741
|
lastAssistantText,
|
|
1960
|
-
promptsDir,
|
|
1961
2742
|
uiBlocked
|
|
1962
2743
|
};
|
|
1963
2744
|
//# sourceMappingURL=mu-pi.js.map
|