mu-agent 0.2.0 → 0.2.2
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 +35 -0
- package/judge/dist/mude.js +277 -81
- package/judge/dist/setup.js +1511 -164
- package/judge/manifest.json +16 -16
- package/kyrn/bin/mu.mjs +8 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.2] - 2026-10-24
|
|
6
|
+
|
|
7
|
+
**0.2.2 makes `mu judge mude install` cope with a full disk, a stop at any step and two installs at once.**
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- English pass on the strings added since 0.1.10, against `docs/writing-style.md`: `mu judge mude` messages say how to resume ("Run mu judge mude install to resume"); errors say what happened and what to do next ("Couldn't create the Python environment (uv venv)", "Hugging Face returned 404 for …", the disk-space error); the kernel's settings descriptions read naturally (their translations kept); `mu --help` describes `mu judge mude install` as "install mude 1.0, mu's own decision model for coding, and make it the judge on this Mac".
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `mu judge mude install`: Ctrl-C (or SIGTERM) during the wait between tries after a dropped connection now stops the download at once. It prints "Stopped with X of 1.8 GB downloaded. Run mu judge mude install to resume". Before, the wait (up to 30 s) ran on, and the command ended 5 s later with no message. What was downloaded was kept either way.
|
|
16
|
+
- `mu judge mude install`, disk space and stops:
|
|
17
|
+
- A full disk (ENOSPC or EDQUOT) during the download now ends the install at once with "The disk is full: couldn't write <file>. Free up some space, then run mu judge mude install to resume". Before, it was retried as a network error for about two minutes, and the desktop app said it couldn't reach Hugging Face.
|
|
18
|
+
- The free-space check before a download counts only the bytes still to come. Before, it counted the whole of every unfinished file, even when most of it was on the disk already.
|
|
19
|
+
- Ctrl-C (or SIGTERM) while the Python environment is being set up now says "Stopped while setting up the Python environment. Run mu judge mude install to resume", instead of "mude's Python packages could not be installed".
|
|
20
|
+
- `mu judge mude install` on a Mac without uv: downloading the pinned uv (about 20 MB, from GitHub) had no time limit and ignored a stop. It now ends after 5 minutes with "uv could not be downloaded from GitHub (no answer in 300 s): run mu judge mude install again", and Ctrl-C or the app's Cancel ends it at once ("Stopped while setting up the Python environment").
|
|
21
|
+
- `mu judge mude install` (also from `mu setup` and the desktop app) takes `local-judge/mude/install.lock`. A second install while one runs now stops at once with "Another mude 1.0 install is running (process N). Wait for it to finish, or stop it, then run mu judge mude install again". Before, both downloaded into the same `.part` files, and the second failed with `ENOENT … .part` when the first renamed a finished file. The lock records the process's pid and start time, so a lock left by a power cut or a kill is taken over, even if the pid has since been reused.
|
|
22
|
+
- `mu judge mude install`, `enable` and `start`: a stop (Ctrl-C, or SIGTERM from the app's Cancel) while waiting for the server's `/health` now ends the wait at once. It also stops the server if this command started it, with "Stopped while the mude 1.0 server was starting, and stopped the server too. Run mu judge mude install again to finish". Before, the wait ran on until the CLI's 5 s exit timer, with no message. The server is started `detached`, so it survived both that exit and the app's process-group kill, and kept about 4 GB of memory while mude was not the judge. A server that was already running before the command is left running: "Stopped waiting for the mude 1.0 server, which is still starting…".
|
|
23
|
+
|
|
24
|
+
## [0.2.1] - 2026-10-24
|
|
25
|
+
|
|
26
|
+
**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.**
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `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.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- `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.
|
|
35
|
+
- A resumed file that fails its sha256 check is downloaded once more from the start, instead of ending the install.
|
|
36
|
+
- 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.
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
|
|
5
40
|
## [0.2.0] - 2026-10-23
|
|
6
41
|
|
|
7
42
|
**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
|
@@ -92,21 +92,21 @@ function readEnvEntries(text) {
|
|
|
92
92
|
}
|
|
93
93
|
let body = rest.slice(1);
|
|
94
94
|
let end = closingQuote(body, quote);
|
|
95
|
-
const
|
|
95
|
+
const startedAt2 = index;
|
|
96
96
|
while (end < 0 && index + 1 < lines.length) {
|
|
97
97
|
body += `
|
|
98
98
|
${lines[++index]}`;
|
|
99
99
|
end = closingQuote(body, quote);
|
|
100
100
|
}
|
|
101
101
|
if (end < 0) {
|
|
102
|
-
index =
|
|
102
|
+
index = startedAt2;
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
105
105
|
const value = body.slice(0, end);
|
|
106
106
|
entries.push({
|
|
107
107
|
name,
|
|
108
108
|
value: quote === '"' ? value.replace(/\\(["\\$`])/g, "$1") : value,
|
|
109
|
-
first:
|
|
109
|
+
first: startedAt2,
|
|
110
110
|
last: index
|
|
111
111
|
});
|
|
112
112
|
}
|
|
@@ -409,12 +409,21 @@ function tail(file, lines = 12) {
|
|
|
409
409
|
return "";
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
|
+
var StartStopped = class extends Error {
|
|
413
|
+
/** Whether the server went too: one this call started does, one that was running before does not. */
|
|
414
|
+
serverStopped;
|
|
415
|
+
constructor(serverStopped = false) {
|
|
416
|
+
super("Stopped while the mude 1.0 server was starting");
|
|
417
|
+
this.name = "StartStopped";
|
|
418
|
+
this.serverStopped = serverStopped;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
412
421
|
async function startServer(paths, options) {
|
|
413
422
|
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
414
423
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
415
424
|
const running = readPid(paths);
|
|
416
425
|
if (running && serverAlive(paths, running)) {
|
|
417
|
-
if (options.wait !== false) await untilReady(paths, running, options.port, timeoutMs, sleep);
|
|
426
|
+
if (options.wait !== false) await untilReady(paths, running, options.port, timeoutMs, sleep, options.signal);
|
|
418
427
|
return { pid: running, port: options.port, already: true };
|
|
419
428
|
}
|
|
420
429
|
if (!await portFree(options.port)) {
|
|
@@ -463,12 +472,21 @@ async function startServer(paths, options) {
|
|
|
463
472
|
child.unref();
|
|
464
473
|
writeFileSync2(paths.pid, `${pid}
|
|
465
474
|
`, { mode: 384 });
|
|
466
|
-
if (options.wait !== false)
|
|
475
|
+
if (options.wait !== false) {
|
|
476
|
+
try {
|
|
477
|
+
await untilReady(paths, pid, options.port, timeoutMs, sleep, options.signal);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (!(error instanceof StartStopped)) throw error;
|
|
480
|
+
await stopServer(paths);
|
|
481
|
+
throw new StartStopped(true);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
467
484
|
return { pid, port: options.port, already: false };
|
|
468
485
|
}
|
|
469
|
-
async function untilReady(paths, pid, port2, timeoutMs, sleep) {
|
|
486
|
+
async function untilReady(paths, pid, port2, timeoutMs, sleep, signal) {
|
|
470
487
|
const deadline = Date.now() + timeoutMs;
|
|
471
488
|
while (Date.now() < deadline) {
|
|
489
|
+
if (signal?.aborted) throw new StartStopped();
|
|
472
490
|
if ((await health(port2))?.status === "ready") return;
|
|
473
491
|
if (!serverAlive(paths, pid)) {
|
|
474
492
|
rmSync(paths.pid, { force: true });
|
|
@@ -641,19 +659,22 @@ function readText(path) {
|
|
|
641
659
|
import { createHash } from "node:crypto";
|
|
642
660
|
import {
|
|
643
661
|
createReadStream,
|
|
644
|
-
createWriteStream,
|
|
645
662
|
existsSync as existsSync3,
|
|
646
663
|
mkdirSync as mkdirSync3,
|
|
647
664
|
readFileSync as readFileSync4,
|
|
648
665
|
renameSync as renameSync2,
|
|
649
666
|
rmSync as rmSync2,
|
|
650
667
|
statSync as statSync3,
|
|
668
|
+
truncateSync,
|
|
651
669
|
writeFileSync as writeFileSync4
|
|
652
670
|
} from "node:fs";
|
|
671
|
+
import { open } from "node:fs/promises";
|
|
653
672
|
import { homedir } from "node:os";
|
|
654
673
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
655
|
-
import { Readable } from "node:stream";
|
|
656
674
|
import { pipeline } from "node:stream/promises";
|
|
675
|
+
import { setTimeout as wait } from "node:timers/promises";
|
|
676
|
+
var CHECKPOINT_BYTES = 32 * 1024 * 1024;
|
|
677
|
+
var RETRY_WAITS = [2e3, 5e3, 1e4, 2e4, 3e4];
|
|
657
678
|
function huggingFaceToken(env = process.env, home = homedir()) {
|
|
658
679
|
const direct = env.HF_TOKEN?.trim();
|
|
659
680
|
if (direct) return direct;
|
|
@@ -755,66 +776,100 @@ async function downloadFiles(options) {
|
|
|
755
776
|
}
|
|
756
777
|
rmSync2(target, { force: true });
|
|
757
778
|
}
|
|
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
779
|
const part = `${target}.part`;
|
|
766
|
-
const
|
|
780
|
+
const onBytes = (bytes) => options.onProgress?.({ file: file.path, checking: false, done: settled + bytes, total });
|
|
781
|
+
let resumed = await fetchFile(options, file, part, onBytes);
|
|
782
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
|
|
783
|
+
let actual = await sha256File(part);
|
|
784
|
+
if (actual !== file.sha256 && resumed) {
|
|
785
|
+
dropPart(part);
|
|
786
|
+
resumed = await fetchFile(options, file, part, onBytes);
|
|
787
|
+
options.onProgress?.({ file: file.path, checking: true, done: settled + file.size, total });
|
|
788
|
+
actual = await sha256File(part);
|
|
789
|
+
}
|
|
767
790
|
if (actual !== file.sha256) {
|
|
768
|
-
|
|
791
|
+
dropPart(part);
|
|
769
792
|
throw new DownloadError(
|
|
770
793
|
`${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
794
|
"integrity"
|
|
772
795
|
);
|
|
773
796
|
}
|
|
774
797
|
renameSync2(part, target);
|
|
798
|
+
rmSync2(savedPath(part), { force: true });
|
|
775
799
|
remember(dir, verified, file);
|
|
776
800
|
settled += file.size;
|
|
777
801
|
}
|
|
778
802
|
options.onProgress?.({ file: "", checking: false, done: total, total });
|
|
779
803
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
804
|
+
var savedPath = (part) => `${part}.saved`;
|
|
805
|
+
function dropPart(part) {
|
|
806
|
+
rmSync2(part, { force: true });
|
|
807
|
+
rmSync2(savedPath(part), { force: true });
|
|
808
|
+
}
|
|
809
|
+
function resumeFrom(part, size, checkpointBytes = CHECKPOINT_BYTES) {
|
|
810
|
+
const have = Math.min(sizeOf(part), size);
|
|
811
|
+
let saved;
|
|
812
|
+
try {
|
|
813
|
+
const value = Number.parseInt(readFileSync4(savedPath(part), "utf8").trim(), 10);
|
|
814
|
+
if (Number.isSafeInteger(value) && value >= 0) saved = value;
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
return saved === void 0 ? Math.max(0, have - checkpointBytes) : Math.min(saved, have);
|
|
818
|
+
}
|
|
819
|
+
function writeSaved(part, bytes) {
|
|
820
|
+
const path = savedPath(part);
|
|
821
|
+
writeFileSync4(`${path}.tmp`, `${bytes}
|
|
822
|
+
`);
|
|
823
|
+
renameSync2(`${path}.tmp`, path);
|
|
824
|
+
}
|
|
825
|
+
async function fetchFile(options, file, part, onBytes) {
|
|
826
|
+
const attempts = options.attempts ?? 8;
|
|
827
|
+
const sleep = options.sleep ?? ((ms) => wait(ms, void 0, { signal: options.signal }).catch(() => {
|
|
828
|
+
}));
|
|
829
|
+
let resumed = false;
|
|
830
|
+
let failures = 0;
|
|
784
831
|
let lastError;
|
|
785
|
-
for (
|
|
832
|
+
for (; ; ) {
|
|
786
833
|
if (options.signal?.aborted) throw new DownloadError("The download was stopped", "aborted");
|
|
834
|
+
const before = resumeFrom(part, file.size, options.checkpointBytes);
|
|
787
835
|
try {
|
|
788
|
-
await fetchOnce(options, file, part, onBytes);
|
|
789
|
-
return;
|
|
836
|
+
resumed = await fetchOnce(options, file, part, onBytes) || resumed;
|
|
837
|
+
return resumed;
|
|
790
838
|
} catch (error) {
|
|
791
839
|
if (error instanceof DownloadError && error.kind !== "network") throw error;
|
|
840
|
+
const code = error.code;
|
|
841
|
+
if (code === "ENOSPC" || code === "EDQUOT") {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`The disk is full: couldn't write ${file.path}. Free up some space, then run mu judge mude install to resume`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
792
846
|
lastError = error;
|
|
793
|
-
|
|
847
|
+
failures = resumeFrom(part, file.size, options.checkpointBytes) > before ? 1 : failures + 1;
|
|
848
|
+
if (failures >= attempts) break;
|
|
849
|
+
await sleep(RETRY_WAITS[Math.min(failures, RETRY_WAITS.length) - 1]);
|
|
794
850
|
}
|
|
795
851
|
}
|
|
796
852
|
const reason = lastError instanceof Error ? lastError.message : String(lastError);
|
|
797
853
|
throw new DownloadError(
|
|
798
|
-
`${file.path} could not be downloaded (${reason})
|
|
854
|
+
`${file.path} could not be downloaded (${reason}). Run mu judge mude install again to resume`,
|
|
799
855
|
"network"
|
|
800
856
|
);
|
|
801
857
|
}
|
|
802
858
|
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;
|
|
859
|
+
const checkpointBytes = options.checkpointBytes ?? CHECKPOINT_BYTES;
|
|
860
|
+
let have = resumeFrom(part, file.size, checkpointBytes);
|
|
861
|
+
if (have === file.size) {
|
|
862
|
+
if (sizeOf(part) > file.size) truncateSync(part, file.size);
|
|
863
|
+
return true;
|
|
811
864
|
}
|
|
812
|
-
if (have === file.size) return;
|
|
813
865
|
const controller = new AbortController();
|
|
814
866
|
const stallMs = options.stallMs ?? 6e4;
|
|
815
867
|
let stall = setTimeout(() => controller.abort(new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
|
|
816
868
|
const onAbort = () => controller.abort(options.signal?.reason);
|
|
817
869
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
870
|
+
let handle;
|
|
871
|
+
let written = 0;
|
|
872
|
+
let synced = 0;
|
|
818
873
|
try {
|
|
819
874
|
const headers = {};
|
|
820
875
|
if (have > 0) headers.Range = `bytes=${have}-`;
|
|
@@ -826,34 +881,56 @@ async function fetchOnce(options, file, part, onBytes) {
|
|
|
826
881
|
});
|
|
827
882
|
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
828
883
|
throw new DownloadError(
|
|
829
|
-
`Hugging Face
|
|
884
|
+
`Hugging Face returned ${response.status} for ${file.path}: the model isn't public yet, or this machine's token can't access it`,
|
|
830
885
|
"access"
|
|
831
886
|
);
|
|
832
887
|
}
|
|
833
|
-
if (response.status === 416
|
|
834
|
-
|
|
888
|
+
if (response.status === 416) {
|
|
889
|
+
dropPart(part);
|
|
890
|
+
throw new Error("HTTP 416");
|
|
835
891
|
}
|
|
836
892
|
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
|
|
837
|
-
const resumed = response.status === 206;
|
|
893
|
+
const resumed = response.status === 206 && have > 0;
|
|
838
894
|
if (!resumed) have = 0;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
895
|
+
mkdirSync3(dirname2(part), { recursive: true });
|
|
896
|
+
handle = await open(part, existsSync3(part) ? "r+" : "w");
|
|
897
|
+
await handle.truncate(have);
|
|
898
|
+
writeSaved(part, have);
|
|
899
|
+
written = have;
|
|
900
|
+
synced = have;
|
|
901
|
+
onBytes(written);
|
|
902
|
+
const reader = response.body.getReader();
|
|
903
|
+
for (; ; ) {
|
|
904
|
+
const { done, value: chunk } = await reader.read();
|
|
905
|
+
if (done) break;
|
|
845
906
|
clearTimeout(stall);
|
|
846
907
|
stall = setTimeout(() => controller.abort(new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
908
|
+
if (written + chunk.length > file.size) throw new Error(`got more than the ${file.size} bytes of the file`);
|
|
909
|
+
await handle.write(chunk, 0, chunk.length, written);
|
|
910
|
+
written += chunk.length;
|
|
911
|
+
if (written - synced >= checkpointBytes) {
|
|
912
|
+
await handle.sync();
|
|
913
|
+
synced = written;
|
|
914
|
+
writeSaved(part, synced);
|
|
915
|
+
}
|
|
916
|
+
onBytes(written);
|
|
917
|
+
}
|
|
918
|
+
if (written !== file.size) throw new Error(`got ${written} of ${file.size} bytes`);
|
|
919
|
+
return resumed;
|
|
851
920
|
} catch (error) {
|
|
852
921
|
if (options.signal?.aborted) throw new DownloadError("The download was stopped", "aborted");
|
|
853
922
|
throw error;
|
|
854
923
|
} finally {
|
|
855
924
|
clearTimeout(stall);
|
|
856
925
|
options.signal?.removeEventListener("abort", onAbort);
|
|
926
|
+
if (handle) {
|
|
927
|
+
try {
|
|
928
|
+
await handle.sync();
|
|
929
|
+
if (written > synced) writeSaved(part, written);
|
|
930
|
+
} catch {
|
|
931
|
+
}
|
|
932
|
+
await handle.close();
|
|
933
|
+
}
|
|
857
934
|
}
|
|
858
935
|
}
|
|
859
936
|
|
|
@@ -874,7 +951,9 @@ function mudePaths(muHome) {
|
|
|
874
951
|
key: join4(root, "key"),
|
|
875
952
|
pid: join4(root, "server.pid"),
|
|
876
953
|
log: join4(root, "server.log"),
|
|
877
|
-
state: join4(root, "state.json")
|
|
954
|
+
state: join4(root, "state.json"),
|
|
955
|
+
install: join4(root, "install.json"),
|
|
956
|
+
installLock: join4(root, "install.lock")
|
|
878
957
|
};
|
|
879
958
|
}
|
|
880
959
|
|
|
@@ -941,9 +1020,20 @@ async function downloadUv(paths, options = {}) {
|
|
|
941
1020
|
mkdirSync4(paths.tools, { recursive: true });
|
|
942
1021
|
const archive = "uv.tar.gz";
|
|
943
1022
|
options.onLine?.(`Downloading uv ${UV_RELEASE.version}`);
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
1023
|
+
const timeoutMs = options.timeoutMs ?? 3e5;
|
|
1024
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
1025
|
+
let bytes;
|
|
1026
|
+
try {
|
|
1027
|
+
const response = await (options.fetch ?? fetch)(UV_RELEASE.url, {
|
|
1028
|
+
redirect: "follow",
|
|
1029
|
+
signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout
|
|
1030
|
+
});
|
|
1031
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1032
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
const reason = timeout.aborted ? `no answer in ${timeoutMs / 1e3} s` : error instanceof Error ? error.message : String(error);
|
|
1035
|
+
throw new Error(`uv could not be downloaded from GitHub (${reason}): run mu judge mude install again`);
|
|
1036
|
+
}
|
|
947
1037
|
const actual = createHash2("sha256").update(bytes).digest("hex");
|
|
948
1038
|
if (actual !== UV_RELEASE.sha256)
|
|
949
1039
|
throw new Error("The downloaded uv does not match its pinned sha256; nothing was installed");
|
|
@@ -958,7 +1048,7 @@ async function downloadUv(paths, options = {}) {
|
|
|
958
1048
|
]);
|
|
959
1049
|
rmSync3(join5(paths.tools, archive), { force: true });
|
|
960
1050
|
if (unpacked.code !== 0 || !executable(join5(paths.tools, "uv")))
|
|
961
|
-
throw new Error(`
|
|
1051
|
+
throw new Error(`Couldn't unpack uv: ${unpacked.output}`);
|
|
962
1052
|
return join5(paths.tools, "uv");
|
|
963
1053
|
}
|
|
964
1054
|
function lockDigest(lockFile) {
|
|
@@ -980,7 +1070,7 @@ async function ensureVenv(paths, lockFile, uv, options = {}) {
|
|
|
980
1070
|
env,
|
|
981
1071
|
onLine: options.onLine
|
|
982
1072
|
});
|
|
983
|
-
if (made.code !== 0) throw new Error(`
|
|
1073
|
+
if (made.code !== 0) throw new Error(`Couldn't create the Python environment (uv venv):
|
|
984
1074
|
${made.output}`);
|
|
985
1075
|
const installed2 = await run(
|
|
986
1076
|
uv,
|
|
@@ -999,7 +1089,7 @@ ${made.output}`);
|
|
|
999
1089
|
{ env, onLine: options.onLine }
|
|
1000
1090
|
);
|
|
1001
1091
|
if (installed2.code !== 0)
|
|
1002
|
-
throw new Error(`mude's Python packages
|
|
1092
|
+
throw new Error(`Couldn't install mude's Python packages (uv pip install):
|
|
1003
1093
|
${installed2.output}`);
|
|
1004
1094
|
writeFileSync5(paths.venvLock, `${lockDigest(lockFile)}
|
|
1005
1095
|
`);
|
|
@@ -1009,7 +1099,8 @@ ${installed2.output}`);
|
|
|
1009
1099
|
var USAGE = `mu judge mude: mude 1.0, mu's own decision model optimized for coding, as the judge on this Mac
|
|
1010
1100
|
(Apple silicon, macOS 14 or later)
|
|
1011
1101
|
|
|
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
|
|
1102
|
+
mu judge mude install [--local-only] download it (${(MUDE_BYTES / 1e9).toFixed(1)} GB), set it up, start it and make it the judge;
|
|
1103
|
+
after a stop (Ctrl-C, a lost connection, a power cut), it picks up where it left off
|
|
1013
1104
|
mu judge mude enable [--local-only | --with-fallback]
|
|
1014
1105
|
make it the judge again, or change what happens when it can't answer
|
|
1015
1106
|
mu judge mude disable back to the judges from before, and stop its server
|
|
@@ -1063,8 +1154,12 @@ function humanOut(write, tty) {
|
|
|
1063
1154
|
line: (text) => write(` ${text}
|
|
1064
1155
|
`),
|
|
1065
1156
|
status: (status2) => write(describe(status2)),
|
|
1066
|
-
error: (message) =>
|
|
1067
|
-
|
|
1157
|
+
error: (message) => {
|
|
1158
|
+
if (shown >= 0 && tty) write("\n");
|
|
1159
|
+
shown = -1;
|
|
1160
|
+
write(`${message}
|
|
1161
|
+
`);
|
|
1162
|
+
}
|
|
1068
1163
|
};
|
|
1069
1164
|
}
|
|
1070
1165
|
function describe(status2) {
|
|
@@ -1075,7 +1170,7 @@ function describe(status2) {
|
|
|
1075
1170
|
`;
|
|
1076
1171
|
}
|
|
1077
1172
|
lines.push(
|
|
1078
|
-
`model ${status2.installed ? `installed (${MUDE_REPO} @ ${MUDE_REVISION.slice(0, 7)})` : status2.downloadedBytes > 0 ? `partly downloaded (${(status2.downloadedBytes / 1e9).toFixed(1)} of ${(MUDE_BYTES / 1e9).toFixed(1)} GB): mu judge mude install
|
|
1173
|
+
`model ${status2.installed ? `installed (${MUDE_REPO} @ ${MUDE_REVISION.slice(0, 7)})` : status2.downloadedBytes > 0 ? `partly downloaded (${(status2.downloadedBytes / 1e9).toFixed(1)} of ${(MUDE_BYTES / 1e9).toFixed(1)} GB): mu judge mude install resumes it` : "not installed: mu judge mude install"}`
|
|
1079
1174
|
);
|
|
1080
1175
|
if (status2.installed && !status2.pythonReady) lines.push("python not set up: mu judge mude install");
|
|
1081
1176
|
lines.push(
|
|
@@ -1129,6 +1224,52 @@ function freeBytes(path) {
|
|
|
1129
1224
|
return void 0;
|
|
1130
1225
|
}
|
|
1131
1226
|
}
|
|
1227
|
+
var gb = (bytes) => (bytes / 1e9).toFixed(1);
|
|
1228
|
+
function installLocalOnly(ctx, args) {
|
|
1229
|
+
if (args.includes("--local-only")) return true;
|
|
1230
|
+
if (args.includes("--with-fallback")) return false;
|
|
1231
|
+
try {
|
|
1232
|
+
const value = JSON.parse(readFileSync6(ctx.paths.install, "utf8"));
|
|
1233
|
+
return typeof value === "object" && value !== null && "localOnly" in value && value.localOnly === true;
|
|
1234
|
+
} catch {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function startedAt(pid) {
|
|
1239
|
+
try {
|
|
1240
|
+
return execFileSync2("/bin/ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8" }).trim() || void 0;
|
|
1241
|
+
} catch {
|
|
1242
|
+
return void 0;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
function holdInstall(paths) {
|
|
1246
|
+
mkdirSync5(paths.root, { recursive: true });
|
|
1247
|
+
const mine = `${JSON.stringify({ pid: process.pid, started: startedAt(process.pid) ?? "" })}
|
|
1248
|
+
`;
|
|
1249
|
+
try {
|
|
1250
|
+
writeFileSync6(paths.installLock, mine, { flag: "wx" });
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
if (error.code !== "EEXIST") throw error;
|
|
1253
|
+
let holder = {};
|
|
1254
|
+
try {
|
|
1255
|
+
holder = JSON.parse(readFileSync6(paths.installLock, "utf8"));
|
|
1256
|
+
} catch {
|
|
1257
|
+
}
|
|
1258
|
+
const pid = typeof holder.pid === "number" ? holder.pid : 0;
|
|
1259
|
+
if (pid > 0 && pid !== process.pid && holder.started && startedAt(pid) === holder.started) {
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`Another mude 1.0 install is running (process ${pid}). Wait for it to finish, or stop it, then run mu judge mude install again`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
writeFileSync6(paths.installLock, mine);
|
|
1265
|
+
}
|
|
1266
|
+
return () => {
|
|
1267
|
+
try {
|
|
1268
|
+
if (readFileSync6(paths.installLock, "utf8") === mine) rmSync4(paths.installLock, { force: true });
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1132
1273
|
function installed(ctx) {
|
|
1133
1274
|
return missingFiles(ctx.paths.model, MUDE_FILES).length === 0;
|
|
1134
1275
|
}
|
|
@@ -1180,29 +1321,52 @@ async function ensureInstalled(ctx, out) {
|
|
|
1180
1321
|
const missing = missingFiles(ctx.paths.model, MUDE_FILES);
|
|
1181
1322
|
if (missing.length > 0) {
|
|
1182
1323
|
const need = missing.reduce((sum, file) => sum + file.size, 0);
|
|
1324
|
+
const already = downloadedBytes(ctx.paths.model, MUDE_FILES);
|
|
1183
1325
|
const free = freeBytes(ctx.paths.root);
|
|
1184
|
-
|
|
1326
|
+
const room = MUDE_BYTES - already + 2e9;
|
|
1327
|
+
if (free !== void 0 && free < room) {
|
|
1185
1328
|
throw new Error(
|
|
1186
|
-
`Not enough free disk space: mude 1.0 needs about ${(
|
|
1329
|
+
`Not enough free disk space: mude 1.0 needs about ${gb(room)} GB, and only ${gb(free)} GB are free. Free up some space, then run mu judge mude install again`
|
|
1187
1330
|
);
|
|
1188
1331
|
}
|
|
1189
1332
|
out.step(
|
|
1190
1333
|
"download",
|
|
1191
|
-
`
|
|
1334
|
+
already > 0 ? `Resuming the mude 1.0 download (${MUDE_REPO}): ${gb(already)} of ${gb(MUDE_BYTES)} GB already downloaded` : `Downloading mude 1.0 (${MUDE_REPO}, ${gb(need)} GB) and checking every file`
|
|
1192
1335
|
);
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1336
|
+
try {
|
|
1337
|
+
await downloadFiles({
|
|
1338
|
+
dir: ctx.paths.model,
|
|
1339
|
+
files: MUDE_FILES,
|
|
1340
|
+
url: (path) => mudeFileUrl(path, ctx.env.HF_ENDPOINT || void 0),
|
|
1341
|
+
token: huggingFaceToken(ctx.env),
|
|
1342
|
+
onProgress: (event) => out.progress(event),
|
|
1343
|
+
signal: ctx.signal
|
|
1344
|
+
});
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
if (error instanceof DownloadError && error.kind === "aborted") {
|
|
1347
|
+
throw new DownloadError(
|
|
1348
|
+
`Stopped with ${gb(downloadedBytes(ctx.paths.model, MUDE_FILES))} of ${gb(MUDE_BYTES)} GB downloaded. Run mu judge mude install to resume`,
|
|
1349
|
+
"aborted"
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1200
1354
|
}
|
|
1201
1355
|
if (!venvReady(ctx.paths, ctx.lockFile)) {
|
|
1202
1356
|
out.step("python", "Setting up the Python environment for mude 1.0 (MLX, transformers; about 1 GB, once)");
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1357
|
+
try {
|
|
1358
|
+
let uv = findUv(ctx.paths, ctx.env);
|
|
1359
|
+
if (!uv) uv = await downloadUv(ctx.paths, { onLine: (line) => out.line(line), signal: ctx.signal });
|
|
1360
|
+
await ensureVenv(ctx.paths, ctx.lockFile, uv, { onLine: (line) => out.line(line), env: ctx.env });
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
if (ctx.signal?.aborted) {
|
|
1363
|
+
throw new DownloadError(
|
|
1364
|
+
"Stopped while setting up the Python environment. Run mu judge mude install to resume",
|
|
1365
|
+
"aborted"
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
throw error;
|
|
1369
|
+
}
|
|
1206
1370
|
}
|
|
1207
1371
|
}
|
|
1208
1372
|
function requireInstalled(ctx) {
|
|
@@ -1210,13 +1374,23 @@ function requireInstalled(ctx) {
|
|
|
1210
1374
|
throw new Error("mude 1.0 is not installed here yet: mu judge mude install");
|
|
1211
1375
|
}
|
|
1212
1376
|
}
|
|
1213
|
-
async function start(ctx, out,
|
|
1377
|
+
async function start(ctx, out, command, wait2 = true) {
|
|
1214
1378
|
const at = await port(ctx);
|
|
1215
1379
|
out.step(
|
|
1216
1380
|
"start",
|
|
1217
1381
|
serverAlive(ctx.paths) ? `The mude 1.0 server is running on 127.0.0.1:${at}` : `Starting the mude 1.0 server on 127.0.0.1:${at} (loading the model takes a few seconds)`
|
|
1218
1382
|
);
|
|
1219
|
-
|
|
1383
|
+
try {
|
|
1384
|
+
await startServer(ctx.paths, { port: at, key: key(ctx), wait: wait2, signal: ctx.signal });
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
if (error instanceof StartStopped) {
|
|
1387
|
+
throw new DownloadError(
|
|
1388
|
+
error.serverStopped ? `Stopped while the mude 1.0 server was starting, and stopped the server too. Run mu judge mude ${command} again to finish` : `Stopped waiting for the mude 1.0 server, which is still starting. Run mu judge mude ${command} again to finish`,
|
|
1389
|
+
"aborted"
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
throw error;
|
|
1393
|
+
}
|
|
1220
1394
|
return at;
|
|
1221
1395
|
}
|
|
1222
1396
|
function enable(ctx, out, at, localOnly) {
|
|
@@ -1253,16 +1427,25 @@ async function main(argv, ctx, write, tty = false) {
|
|
|
1253
1427
|
}
|
|
1254
1428
|
switch (command) {
|
|
1255
1429
|
case "install": {
|
|
1256
|
-
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1430
|
+
const localOnly = installLocalOnly(ctx, args);
|
|
1431
|
+
const release2 = holdInstall(ctx.paths);
|
|
1432
|
+
try {
|
|
1433
|
+
writeFileSync6(ctx.paths.install, `${JSON.stringify({ localOnly })}
|
|
1434
|
+
`);
|
|
1435
|
+
await ensureInstalled(ctx, out);
|
|
1436
|
+
const at = await start(ctx, out, "install");
|
|
1437
|
+
enable(ctx, out, at, localOnly);
|
|
1438
|
+
rmSync4(ctx.paths.install, { force: true });
|
|
1439
|
+
} finally {
|
|
1440
|
+
release2();
|
|
1441
|
+
}
|
|
1259
1442
|
break;
|
|
1260
1443
|
}
|
|
1261
1444
|
case "enable": {
|
|
1262
1445
|
requireInstalled(ctx);
|
|
1263
1446
|
const current = judgeSetting(ctx.files);
|
|
1264
1447
|
const localOnly = flag("--local-only") ? true : flag("--with-fallback") ? false : current.enabled && current.localOnly;
|
|
1265
|
-
const at = await start(ctx, out);
|
|
1448
|
+
const at = await start(ctx, out, "enable");
|
|
1266
1449
|
enable(ctx, out, at, localOnly);
|
|
1267
1450
|
break;
|
|
1268
1451
|
}
|
|
@@ -1282,7 +1465,7 @@ async function main(argv, ctx, write, tty = false) {
|
|
|
1282
1465
|
}
|
|
1283
1466
|
case "start": {
|
|
1284
1467
|
requireInstalled(ctx);
|
|
1285
|
-
await start(ctx, out, !flag("--no-wait"));
|
|
1468
|
+
await start(ctx, out, "start", !flag("--no-wait"));
|
|
1286
1469
|
break;
|
|
1287
1470
|
}
|
|
1288
1471
|
case "stop": {
|
|
@@ -1326,7 +1509,20 @@ function contextFromEnv(env = process.env, home = homedir3()) {
|
|
|
1326
1509
|
}
|
|
1327
1510
|
|
|
1328
1511
|
// packages/kyrn-judge/src/mude/cli.ts
|
|
1329
|
-
|
|
1512
|
+
var stop = new AbortController();
|
|
1513
|
+
var onSignal = (signal) => {
|
|
1514
|
+
if (stop.signal.aborted) process.exit(signal === "SIGINT" ? 130 : 143);
|
|
1515
|
+
stop.abort();
|
|
1516
|
+
setTimeout(() => process.exit(signal === "SIGINT" ? 130 : 143), 5e3).unref();
|
|
1517
|
+
};
|
|
1518
|
+
process.on("SIGINT", onSignal);
|
|
1519
|
+
process.on("SIGTERM", onSignal);
|
|
1520
|
+
main(
|
|
1521
|
+
process.argv.slice(2),
|
|
1522
|
+
{ ...contextFromEnv(), signal: stop.signal },
|
|
1523
|
+
(text) => process.stdout.write(text),
|
|
1524
|
+
Boolean(process.stdout.isTTY)
|
|
1525
|
+
).then(
|
|
1330
1526
|
(code) => {
|
|
1331
1527
|
process.exitCode = code;
|
|
1332
1528
|
},
|