mu-agent 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/judge/dist/mude.js +152 -54
- package/judge/dist/setup.js +1409 -160
- package/kyrn/bin/mu.mjs +6 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.1] - 2026-10-24
|
|
6
|
+
|
|
7
|
+
**0.2.1 makes mude 1.0 the recommended judge on a Mac, set up from `mu setup`, and its install survives a power cut.**
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `mu setup` offers mude 1.0 in its judge step on a Mac that can run it (Apple silicon, macOS 14 or later, 8 GB of memory or more). It is the first choice and the recommended one, so pressing Enter picks it; the Jev that was recommended before follows it and stays a choice. Picking it writes Jev as the judge, then installs mude 1.0 in front of it in the same terminal, with its progress. If the install doesn't finish, Jev stays the judge and `mu judge mude install` goes on from where it stopped. Scripts use `--judge mude`. On other machines the judge step is as before: free Jev is first and recommended.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `mu judge mude install` survives a power cut in the middle of the download. Every 32 MiB the partly downloaded file is synced to the disk and the amount on disk is recorded in `<file>.part.saved`. The next install goes on from there, so it never builds on an end the disk didn't keep. A part left by mu 0.2.0, which has no record, goes on from 32 MiB before its end.
|
|
16
|
+
- A resumed file that fails its sha256 check is downloaded once more from the start, instead of ending the install.
|
|
17
|
+
- The download rides out a lost connection: a try that gets some of the file starts the count of failed tries again. Tries that get nothing wait 2, 5, 10, 20 and then 30 s, about two minutes in all, before the install stops.
|
|
18
|
+
- Ctrl-C, or the desktop app ending an install, stops the download where it is, keeps what it got and says how much: "Stopped with 0.9 of 1.8 GB downloaded: mu judge mude install goes on from there". An install that goes on says how much is already there.
|
|
19
|
+
- An install that goes on from one that stopped keeps that one's `--local-only`: it is recorded in `install.json` until the install finishes. Before, running it again without the flag left the hosted judges behind mude.
|
|
20
|
+
|
|
5
21
|
## [0.2.0] - 2026-10-23
|
|
6
22
|
|
|
7
23
|
**0.2.0 starts the era of mu's own models: `mu judge mude install` turns on mude 1.0 in one command.**
|
package/judge/dist/mude.js
CHANGED
|
@@ -641,19 +641,21 @@ function readText(path) {
|
|
|
641
641
|
import { createHash } from "node:crypto";
|
|
642
642
|
import {
|
|
643
643
|
createReadStream,
|
|
644
|
-
createWriteStream,
|
|
645
644
|
existsSync as existsSync3,
|
|
646
645
|
mkdirSync as mkdirSync3,
|
|
647
646
|
readFileSync as readFileSync4,
|
|
648
647
|
renameSync as renameSync2,
|
|
649
648
|
rmSync as rmSync2,
|
|
650
649
|
statSync as statSync3,
|
|
650
|
+
truncateSync,
|
|
651
651
|
writeFileSync as writeFileSync4
|
|
652
652
|
} from "node:fs";
|
|
653
|
+
import { open } from "node:fs/promises";
|
|
653
654
|
import { homedir } from "node:os";
|
|
654
655
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
655
|
-
import { Readable } from "node:stream";
|
|
656
656
|
import { pipeline } from "node:stream/promises";
|
|
657
|
+
var CHECKPOINT_BYTES = 32 * 1024 * 1024;
|
|
658
|
+
var RETRY_WAITS = [2e3, 5e3, 1e4, 2e4, 3e4];
|
|
657
659
|
function huggingFaceToken(env = process.env, home = homedir()) {
|
|
658
660
|
const direct = env.HF_TOKEN?.trim();
|
|
659
661
|
if (direct) return direct;
|
|
@@ -755,42 +757,70 @@ async function downloadFiles(options) {
|
|
|
755
757
|
}
|
|
756
758
|
rmSync2(target, { force: true });
|
|
757
759
|
}
|
|
758
|
-
await fetchFile(
|
|
759
|
-
options,
|
|
760
|
-
file,
|
|
761
|
-
target,
|
|
762
|
-
(bytes) => options.onProgress?.({ file: file.path, checking: false, done: settled + bytes, total })
|
|
763
|
-
);
|
|
764
|
-
options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
|
|
765
760
|
const part = `${target}.part`;
|
|
766
|
-
const
|
|
761
|
+
const onBytes = (bytes) => options.onProgress?.({ file: file.path, checking: false, done: settled + bytes, total });
|
|
762
|
+
let resumed = await fetchFile(options, file, part, onBytes);
|
|
763
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
|
|
764
|
+
let actual = await sha256File(part);
|
|
765
|
+
if (actual !== file.sha256 && resumed) {
|
|
766
|
+
dropPart(part);
|
|
767
|
+
resumed = await fetchFile(options, file, part, onBytes);
|
|
768
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
|
|
769
|
+
actual = await sha256File(part);
|
|
770
|
+
}
|
|
767
771
|
if (actual !== file.sha256) {
|
|
768
|
-
|
|
772
|
+
dropPart(part);
|
|
769
773
|
throw new DownloadError(
|
|
770
774
|
`${file.path} does not match its pinned sha256 (got ${actual.slice(0, 12)}\u2026, want ${file.sha256.slice(0, 12)}\u2026); it was removed`,
|
|
771
775
|
"integrity"
|
|
772
776
|
);
|
|
773
777
|
}
|
|
774
778
|
renameSync2(part, target);
|
|
779
|
+
rmSync2(savedPath(part), { force: true });
|
|
775
780
|
remember(dir, verified, file);
|
|
776
781
|
settled += file.size;
|
|
777
782
|
}
|
|
778
783
|
options.onProgress?.({ file: "", checking: false, done: total, total });
|
|
779
784
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
785
|
+
var savedPath = (part) => `${part}.saved`;
|
|
786
|
+
function dropPart(part) {
|
|
787
|
+
rmSync2(part, { force: true });
|
|
788
|
+
rmSync2(savedPath(part), { force: true });
|
|
789
|
+
}
|
|
790
|
+
function resumeFrom(part, size, checkpointBytes = CHECKPOINT_BYTES) {
|
|
791
|
+
const have = Math.min(sizeOf(part), size);
|
|
792
|
+
let saved;
|
|
793
|
+
try {
|
|
794
|
+
const value = Number.parseInt(readFileSync4(savedPath(part), "utf8").trim(), 10);
|
|
795
|
+
if (Number.isSafeInteger(value) && value >= 0) saved = value;
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
return saved === void 0 ? Math.max(0, have - checkpointBytes) : Math.min(saved, have);
|
|
799
|
+
}
|
|
800
|
+
function writeSaved(part, bytes) {
|
|
801
|
+
const path = savedPath(part);
|
|
802
|
+
writeFileSync4(`${path}.tmp`, `${bytes}
|
|
803
|
+
`);
|
|
804
|
+
renameSync2(`${path}.tmp`, path);
|
|
805
|
+
}
|
|
806
|
+
async function fetchFile(options, file, part, onBytes) {
|
|
807
|
+
const attempts = options.attempts ?? 8;
|
|
783
808
|
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
809
|
+
let resumed = false;
|
|
810
|
+
let failures = 0;
|
|
784
811
|
let lastError;
|
|
785
|
-
for (
|
|
812
|
+
for (; ; ) {
|
|
786
813
|
if (options.signal?.aborted) throw new DownloadError("The download was stopped", "aborted");
|
|
814
|
+
const before = resumeFrom(part, file.size, options.checkpointBytes);
|
|
787
815
|
try {
|
|
788
|
-
await fetchOnce(options, file, part, onBytes);
|
|
789
|
-
return;
|
|
816
|
+
resumed = await fetchOnce(options, file, part, onBytes) || resumed;
|
|
817
|
+
return resumed;
|
|
790
818
|
} catch (error) {
|
|
791
819
|
if (error instanceof DownloadError && error.kind !== "network") throw error;
|
|
792
820
|
lastError = error;
|
|
793
|
-
|
|
821
|
+
failures = resumeFrom(part, file.size, options.checkpointBytes) > before ? 1 : failures + 1;
|
|
822
|
+
if (failures >= attempts) break;
|
|
823
|
+
await sleep(RETRY_WAITS[Math.min(failures, RETRY_WAITS.length) - 1]);
|
|
794
824
|
}
|
|
795
825
|
}
|
|
796
826
|
const reason = lastError instanceof Error ? lastError.message : String(lastError);
|
|
@@ -800,21 +830,20 @@ async function fetchFile(options, file, target, onBytes) {
|
|
|
800
830
|
);
|
|
801
831
|
}
|
|
802
832
|
async function fetchOnce(options, file, part, onBytes) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (have > file.size) {
|
|
809
|
-
rmSync2(part, { force: true });
|
|
810
|
-
have = 0;
|
|
833
|
+
const checkpointBytes = options.checkpointBytes ?? CHECKPOINT_BYTES;
|
|
834
|
+
let have = resumeFrom(part, file.size, checkpointBytes);
|
|
835
|
+
if (have === file.size) {
|
|
836
|
+
if (sizeOf(part) > file.size) truncateSync(part, file.size);
|
|
837
|
+
return true;
|
|
811
838
|
}
|
|
812
|
-
if (have === file.size) return;
|
|
813
839
|
const controller = new AbortController();
|
|
814
840
|
const stallMs = options.stallMs ?? 6e4;
|
|
815
841
|
let stall = setTimeout(() => controller.abort(new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
|
|
816
842
|
const onAbort = () => controller.abort(options.signal?.reason);
|
|
817
843
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
844
|
+
let handle;
|
|
845
|
+
let written = 0;
|
|
846
|
+
let synced = 0;
|
|
818
847
|
try {
|
|
819
848
|
const headers = {};
|
|
820
849
|
if (have > 0) headers.Range = `bytes=${have}-`;
|
|
@@ -830,30 +859,52 @@ async function fetchOnce(options, file, part, onBytes) {
|
|
|
830
859
|
"access"
|
|
831
860
|
);
|
|
832
861
|
}
|
|
833
|
-
if (response.status === 416
|
|
834
|
-
|
|
862
|
+
if (response.status === 416) {
|
|
863
|
+
dropPart(part);
|
|
864
|
+
throw new Error("HTTP 416");
|
|
835
865
|
}
|
|
836
866
|
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
|
|
837
|
-
const resumed = response.status === 206;
|
|
867
|
+
const resumed = response.status === 206 && have > 0;
|
|
838
868
|
if (!resumed) have = 0;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
869
|
+
mkdirSync3(dirname2(part), { recursive: true });
|
|
870
|
+
handle = await open(part, existsSync3(part) ? "r+" : "w");
|
|
871
|
+
await handle.truncate(have);
|
|
872
|
+
writeSaved(part, have);
|
|
873
|
+
written = have;
|
|
874
|
+
synced = have;
|
|
875
|
+
onBytes(written);
|
|
876
|
+
const reader = response.body.getReader();
|
|
877
|
+
for (; ; ) {
|
|
878
|
+
const { done, value: chunk } = await reader.read();
|
|
879
|
+
if (done) break;
|
|
845
880
|
clearTimeout(stall);
|
|
846
881
|
stall = setTimeout(() => controller.abort(new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
882
|
+
if (written + chunk.length > file.size) throw new Error(`got more than the ${file.size} bytes of the file`);
|
|
883
|
+
await handle.write(chunk, 0, chunk.length, written);
|
|
884
|
+
written += chunk.length;
|
|
885
|
+
if (written - synced >= checkpointBytes) {
|
|
886
|
+
await handle.sync();
|
|
887
|
+
synced = written;
|
|
888
|
+
writeSaved(part, synced);
|
|
889
|
+
}
|
|
890
|
+
onBytes(written);
|
|
891
|
+
}
|
|
892
|
+
if (written !== file.size) throw new Error(`got ${written} of ${file.size} bytes`);
|
|
893
|
+
return resumed;
|
|
851
894
|
} catch (error) {
|
|
852
895
|
if (options.signal?.aborted) throw new DownloadError("The download was stopped", "aborted");
|
|
853
896
|
throw error;
|
|
854
897
|
} finally {
|
|
855
898
|
clearTimeout(stall);
|
|
856
899
|
options.signal?.removeEventListener("abort", onAbort);
|
|
900
|
+
if (handle) {
|
|
901
|
+
try {
|
|
902
|
+
await handle.sync();
|
|
903
|
+
if (written > synced) writeSaved(part, written);
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
await handle.close();
|
|
907
|
+
}
|
|
857
908
|
}
|
|
858
909
|
}
|
|
859
910
|
|
|
@@ -874,7 +925,8 @@ function mudePaths(muHome) {
|
|
|
874
925
|
key: join4(root, "key"),
|
|
875
926
|
pid: join4(root, "server.pid"),
|
|
876
927
|
log: join4(root, "server.log"),
|
|
877
|
-
state: join4(root, "state.json")
|
|
928
|
+
state: join4(root, "state.json"),
|
|
929
|
+
install: join4(root, "install.json")
|
|
878
930
|
};
|
|
879
931
|
}
|
|
880
932
|
|
|
@@ -1009,7 +1061,8 @@ ${installed2.output}`);
|
|
|
1009
1061
|
var USAGE = `mu judge mude: mude 1.0, mu's own decision model optimized for coding, as the judge on this Mac
|
|
1010
1062
|
(Apple silicon, macOS 14 or later)
|
|
1011
1063
|
|
|
1012
|
-
mu judge mude install [--local-only] download it (${(MUDE_BYTES / 1e9).toFixed(1)} GB), set it up, start it and make it the judge
|
|
1064
|
+
mu judge mude install [--local-only] download it (${(MUDE_BYTES / 1e9).toFixed(1)} GB), set it up, start it and make it the judge;
|
|
1065
|
+
after a stop (Ctrl-C, a lost connection, a power cut), it goes on from there
|
|
1013
1066
|
mu judge mude enable [--local-only | --with-fallback]
|
|
1014
1067
|
make it the judge again, or change what happens when it can't answer
|
|
1015
1068
|
mu judge mude disable back to the judges from before, and stop its server
|
|
@@ -1063,8 +1116,12 @@ function humanOut(write, tty) {
|
|
|
1063
1116
|
line: (text) => write(` ${text}
|
|
1064
1117
|
`),
|
|
1065
1118
|
status: (status2) => write(describe(status2)),
|
|
1066
|
-
error: (message) =>
|
|
1067
|
-
|
|
1119
|
+
error: (message) => {
|
|
1120
|
+
if (shown >= 0 && tty) write("\n");
|
|
1121
|
+
shown = -1;
|
|
1122
|
+
write(`${message}
|
|
1123
|
+
`);
|
|
1124
|
+
}
|
|
1068
1125
|
};
|
|
1069
1126
|
}
|
|
1070
1127
|
function describe(status2) {
|
|
@@ -1129,6 +1186,17 @@ function freeBytes(path) {
|
|
|
1129
1186
|
return void 0;
|
|
1130
1187
|
}
|
|
1131
1188
|
}
|
|
1189
|
+
var gb = (bytes) => (bytes / 1e9).toFixed(1);
|
|
1190
|
+
function installLocalOnly(ctx, args) {
|
|
1191
|
+
if (args.includes("--local-only")) return true;
|
|
1192
|
+
if (args.includes("--with-fallback")) return false;
|
|
1193
|
+
try {
|
|
1194
|
+
const value = JSON.parse(readFileSync6(ctx.paths.install, "utf8"));
|
|
1195
|
+
return typeof value === "object" && value !== null && "localOnly" in value && value.localOnly === true;
|
|
1196
|
+
} catch {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1132
1200
|
function installed(ctx) {
|
|
1133
1201
|
return missingFiles(ctx.paths.model, MUDE_FILES).length === 0;
|
|
1134
1202
|
}
|
|
@@ -1186,17 +1254,29 @@ async function ensureInstalled(ctx, out) {
|
|
|
1186
1254
|
`Not enough free disk space: mude 1.0 needs about ${((need + 2e9) / 1e9).toFixed(1)} GB, and ${(free / 1e9).toFixed(1)} GB are free`
|
|
1187
1255
|
);
|
|
1188
1256
|
}
|
|
1257
|
+
const already = downloadedBytes(ctx.paths.model, MUDE_FILES);
|
|
1189
1258
|
out.step(
|
|
1190
1259
|
"download",
|
|
1191
|
-
`
|
|
1260
|
+
already > 0 ? `Going on with the mude 1.0 download (${MUDE_REPO}): ${gb(already)} of ${gb(MUDE_BYTES)} GB are here already` : `Downloading mude 1.0 (${MUDE_REPO}, ${gb(need)} GB) and checking every file`
|
|
1192
1261
|
);
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1262
|
+
try {
|
|
1263
|
+
await downloadFiles({
|
|
1264
|
+
dir: ctx.paths.model,
|
|
1265
|
+
files: MUDE_FILES,
|
|
1266
|
+
url: (path) => mudeFileUrl(path, ctx.env.HF_ENDPOINT || void 0),
|
|
1267
|
+
token: huggingFaceToken(ctx.env),
|
|
1268
|
+
onProgress: (event) => out.progress(event),
|
|
1269
|
+
signal: ctx.signal
|
|
1270
|
+
});
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
if (error instanceof DownloadError && error.kind === "aborted") {
|
|
1273
|
+
throw new DownloadError(
|
|
1274
|
+
`Stopped with ${gb(downloadedBytes(ctx.paths.model, MUDE_FILES))} of ${gb(MUDE_BYTES)} GB downloaded: mu judge mude install goes on from there`,
|
|
1275
|
+
"aborted"
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1200
1280
|
}
|
|
1201
1281
|
if (!venvReady(ctx.paths, ctx.lockFile)) {
|
|
1202
1282
|
out.step("python", "Setting up the Python environment for mude 1.0 (MLX, transformers; about 1 GB, once)");
|
|
@@ -1253,9 +1333,14 @@ async function main(argv, ctx, write, tty = false) {
|
|
|
1253
1333
|
}
|
|
1254
1334
|
switch (command) {
|
|
1255
1335
|
case "install": {
|
|
1336
|
+
const localOnly = installLocalOnly(ctx, args);
|
|
1337
|
+
mkdirSync5(ctx.paths.root, { recursive: true });
|
|
1338
|
+
writeFileSync6(ctx.paths.install, `${JSON.stringify({ localOnly })}
|
|
1339
|
+
`);
|
|
1256
1340
|
await ensureInstalled(ctx, out);
|
|
1257
1341
|
const at = await start(ctx, out);
|
|
1258
|
-
enable(ctx, out, at,
|
|
1342
|
+
enable(ctx, out, at, localOnly);
|
|
1343
|
+
rmSync4(ctx.paths.install, { force: true });
|
|
1259
1344
|
break;
|
|
1260
1345
|
}
|
|
1261
1346
|
case "enable": {
|
|
@@ -1326,7 +1411,20 @@ function contextFromEnv(env = process.env, home = homedir3()) {
|
|
|
1326
1411
|
}
|
|
1327
1412
|
|
|
1328
1413
|
// packages/kyrn-judge/src/mude/cli.ts
|
|
1329
|
-
|
|
1414
|
+
var stop = new AbortController();
|
|
1415
|
+
var onSignal = (signal) => {
|
|
1416
|
+
if (stop.signal.aborted) process.exit(signal === "SIGINT" ? 130 : 143);
|
|
1417
|
+
stop.abort();
|
|
1418
|
+
setTimeout(() => process.exit(signal === "SIGINT" ? 130 : 143), 5e3).unref();
|
|
1419
|
+
};
|
|
1420
|
+
process.on("SIGINT", onSignal);
|
|
1421
|
+
process.on("SIGTERM", onSignal);
|
|
1422
|
+
main(
|
|
1423
|
+
process.argv.slice(2),
|
|
1424
|
+
{ ...contextFromEnv(), signal: stop.signal },
|
|
1425
|
+
(text) => process.stdout.write(text),
|
|
1426
|
+
Boolean(process.stdout.isTTY)
|
|
1427
|
+
).then(
|
|
1330
1428
|
(code) => {
|
|
1331
1429
|
process.exitCode = code;
|
|
1332
1430
|
},
|