@mu-crew/mu 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +466 -249
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +244 -107
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/docs/VISION.md +3 -1
- package/docs/architecture/ops-log.md +26 -7
- package/docs/guide/cleanup.md +19 -0
- package/docs/guide/delegate.md +15 -3
- package/docs/reference/env.md +1 -1
- package/package.json +1 -1
- package/skills/mu/SKILL.md +2 -2
- package/skills/mu/recipes/orchestrator-loop.md +8 -2
- package/skills/mu/recipes/recovery.md +3 -0
- package/skills/mu/recipes/tasks-or-calls.md +9 -5
package/dist/cli.js
CHANGED
|
@@ -175,6 +175,11 @@ function fullOldPayload(cols) {
|
|
|
175
175
|
const pairs = cols.map((c) => `'${c}', OLD.${c}`).join(", ");
|
|
176
176
|
return `json_object(${pairs})`;
|
|
177
177
|
}
|
|
178
|
+
function noteTombstonePayload(keyExpr, cols) {
|
|
179
|
+
return `CASE WHEN EXISTS (
|
|
180
|
+
SELECT 1 FROM ops WHERE entity = 'note' AND key = ${keyExpr} AND op = 'put'
|
|
181
|
+
) THEN '{}' ELSE ${fullOldPayload(cols)} END`;
|
|
182
|
+
}
|
|
178
183
|
function anyChanged(cols) {
|
|
179
184
|
return cols.map((c) => `NEW.${c} IS NOT OLD.${c}`).join("\n OR ");
|
|
180
185
|
}
|
|
@@ -258,7 +263,7 @@ END;
|
|
|
258
263
|
CREATE TEMP TRIGGER IF NOT EXISTS _cap_task_notes_del
|
|
259
264
|
BEFORE DELETE ON task_notes WHEN ${NOT_APPLYING}
|
|
260
265
|
BEGIN
|
|
261
|
-
${emitOp("note", "del", `${taskKey("OLD.task_id")} || '#' || OLD.id`,
|
|
266
|
+
${emitOp("note", "del", `${taskKey("OLD.task_id")} || '#' || OLD.id`, noteTombstonePayload(`${taskKey("OLD.task_id")} || '#' || OLD.id`, noteCols))}
|
|
262
267
|
END;
|
|
263
268
|
|
|
264
269
|
-- \u2500\u2500\u2500 task_edges \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -7620,7 +7625,10 @@ var init_adopt = __esm({
|
|
|
7620
7625
|
// src/agents/delegate.ts
|
|
7621
7626
|
function delegateOutcome(agent, timedOut) {
|
|
7622
7627
|
if (agent.dead) return "died";
|
|
7623
|
-
if (agent.fired)
|
|
7628
|
+
if (agent.fired) {
|
|
7629
|
+
if (agent.lastError !== void 0) return "error";
|
|
7630
|
+
return agent.lastText ? "done" : "empty";
|
|
7631
|
+
}
|
|
7624
7632
|
return timedOut ? "timeout" : "pending";
|
|
7625
7633
|
}
|
|
7626
7634
|
var init_delegate = __esm({
|
|
@@ -7833,6 +7841,7 @@ async function waitForAgents(db, input, opts) {
|
|
|
7833
7841
|
st.wasBusy = true;
|
|
7834
7842
|
st.fired = true;
|
|
7835
7843
|
if (snap.lastText !== void 0) st.lastText = snap.lastText;
|
|
7844
|
+
if (snap.lastError !== void 0) st.lastError = snap.lastError;
|
|
7836
7845
|
}
|
|
7837
7846
|
wake();
|
|
7838
7847
|
},
|
|
@@ -10400,13 +10409,13 @@ import {
|
|
|
10400
10409
|
appendFileSync,
|
|
10401
10410
|
existsSync as existsSync14,
|
|
10402
10411
|
mkdirSync as mkdirSync5,
|
|
10403
|
-
readdirSync as
|
|
10412
|
+
readdirSync as readdirSync4,
|
|
10404
10413
|
readFileSync as readFileSync3,
|
|
10405
|
-
statSync as
|
|
10414
|
+
statSync as statSync3,
|
|
10406
10415
|
writeFileSync as writeFileSync2
|
|
10407
10416
|
} from "fs";
|
|
10408
10417
|
import { tmpdir as tmpdir2 } from "os";
|
|
10409
|
-
import { join as
|
|
10418
|
+
import { join as join13, resolve as resolve6, sep as sep2 } from "path";
|
|
10410
10419
|
import { crc32 } from "zlib";
|
|
10411
10420
|
function syncDir() {
|
|
10412
10421
|
const dir = process.env.MU_SYNC_DIR;
|
|
@@ -10430,7 +10439,7 @@ function localMachineId(db) {
|
|
|
10430
10439
|
return row2.machine_id;
|
|
10431
10440
|
}
|
|
10432
10441
|
function segmentPath(dir, machineId2) {
|
|
10433
|
-
return
|
|
10442
|
+
return join13(dir, `${machineId2}${SEGMENT_EXT}`);
|
|
10434
10443
|
}
|
|
10435
10444
|
function manifestPath(segment) {
|
|
10436
10445
|
return segment.replace(new RegExp(`${SEGMENT_EXT}$`), MANIFEST_EXT);
|
|
@@ -10574,7 +10583,7 @@ async function flushSegment(db, dir = syncDir()) {
|
|
|
10574
10583
|
const path2 = segmentPath(dir, machineId2);
|
|
10575
10584
|
const lockName = createHash2("sha256").update(`${dir}${machineId2}`).digest("hex").slice(0, 16);
|
|
10576
10585
|
return withFileLock(
|
|
10577
|
-
|
|
10586
|
+
join13(locksDir(), `segment-${lockName}.lock`),
|
|
10578
10587
|
`segment:${machineId2}`,
|
|
10579
10588
|
() => Promise.resolve(flushLocked(db, path2, machineId2))
|
|
10580
10589
|
);
|
|
@@ -10696,16 +10705,16 @@ function verifyAgainstManifest(segment) {
|
|
|
10696
10705
|
function discoverPeers(dir, selfMachineId) {
|
|
10697
10706
|
if (!existsSync14(dir)) return [];
|
|
10698
10707
|
const peers = [];
|
|
10699
|
-
for (const name of
|
|
10708
|
+
for (const name of readdirSync4(dir)) {
|
|
10700
10709
|
if (!name.endsWith(SEGMENT_EXT)) continue;
|
|
10701
10710
|
const stem = name.slice(0, -SEGMENT_EXT.length);
|
|
10702
10711
|
const conflictAt = stem.indexOf(".sync-conflict-");
|
|
10703
10712
|
const conflictCopy = conflictAt > 0;
|
|
10704
10713
|
const machineId2 = conflictCopy ? stem.slice(0, conflictAt) : stem;
|
|
10705
10714
|
if (machineId2 === selfMachineId && !conflictCopy) continue;
|
|
10706
|
-
const path2 =
|
|
10715
|
+
const path2 = join13(dir, name);
|
|
10707
10716
|
try {
|
|
10708
|
-
if (!
|
|
10717
|
+
if (!statSync3(path2).isFile()) continue;
|
|
10709
10718
|
} catch {
|
|
10710
10719
|
continue;
|
|
10711
10720
|
}
|
|
@@ -10910,7 +10919,7 @@ var init_segments = __esm({
|
|
|
10910
10919
|
});
|
|
10911
10920
|
|
|
10912
10921
|
// src/sync.ts
|
|
10913
|
-
import { existsSync as existsSync15, statSync as
|
|
10922
|
+
import { existsSync as existsSync15, statSync as statSync4 } from "fs";
|
|
10914
10923
|
import Database2 from "better-sqlite3";
|
|
10915
10924
|
function peerStatuses(db, dir) {
|
|
10916
10925
|
const self = localMachineId(db);
|
|
@@ -10918,7 +10927,7 @@ function peerStatuses(db, dir) {
|
|
|
10918
10927
|
const rows = discoverPeers(dir, self).map((peer) => {
|
|
10919
10928
|
let lastSeenMs = null;
|
|
10920
10929
|
try {
|
|
10921
|
-
lastSeenMs =
|
|
10930
|
+
lastSeenMs = statSync4(peer.path).mtimeMs;
|
|
10922
10931
|
} catch {
|
|
10923
10932
|
lastSeenMs = null;
|
|
10924
10933
|
}
|
|
@@ -20677,7 +20686,8 @@ async function cmdAgentWait(db, rawNames, opts) {
|
|
|
20677
20686
|
const settled = ctlRequest(sock, { op: "wait", afterRuns: probe.status.runs }, { signal }).then(
|
|
20678
20687
|
(reply) => reply.ok && reply.state !== void 0 ? {
|
|
20679
20688
|
status: ctlRuntimeState(reply.state),
|
|
20680
|
-
...reply.lastText !== void 0 ? { lastText: reply.lastText } : {}
|
|
20689
|
+
...reply.lastText !== void 0 ? { lastText: reply.lastText } : {},
|
|
20690
|
+
...reply.lastError !== void 0 ? { lastError: reply.lastError } : {}
|
|
20681
20691
|
} : { status: "unknown", unknownReason: reply.ok ? "ctl wait: no state" : reply.error },
|
|
20682
20692
|
// The connection dropped. A socket that still answers (pi's /new
|
|
20683
20693
|
// restarts the session) hands back to polling; one that does not
|
|
@@ -20886,8 +20896,308 @@ function wireSelfCommands(program) {
|
|
|
20886
20896
|
}
|
|
20887
20897
|
|
|
20888
20898
|
// src/cli/db.ts
|
|
20889
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync6 } from "fs";
|
|
20899
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, statSync as statSync5 } from "fs";
|
|
20890
20900
|
import { dirname as dirname13, resolve as resolve7 } from "path";
|
|
20901
|
+
|
|
20902
|
+
// src/compact.ts
|
|
20903
|
+
init_legacy_ops();
|
|
20904
|
+
var REDUNDANT_TOMBSTONES_SQL = `
|
|
20905
|
+
SELECT d.seq AS seq, length(d.payload) AS bytes
|
|
20906
|
+
FROM ops d
|
|
20907
|
+
WHERE d.entity = 'note' AND d.op = 'del' AND d.payload <> '{}'
|
|
20908
|
+
AND EXISTS (SELECT 1 FROM ops p
|
|
20909
|
+
WHERE p.entity = 'note' AND p.key = d.key AND p.op = 'put')`;
|
|
20910
|
+
function planCompact(db) {
|
|
20911
|
+
const r = db.prepare(
|
|
20912
|
+
`SELECT COUNT(*) AS n, COALESCE(SUM(bytes), 0) AS b FROM (${REDUNDANT_TOMBSTONES_SQL})`
|
|
20913
|
+
).get();
|
|
20914
|
+
return { tombstones: r.n, bytes: r.b };
|
|
20915
|
+
}
|
|
20916
|
+
function compact(db) {
|
|
20917
|
+
const plan = planCompact(db);
|
|
20918
|
+
db.transaction(() => {
|
|
20919
|
+
db.prepare(
|
|
20920
|
+
`UPDATE ops SET payload = '{}' WHERE seq IN (SELECT seq FROM (${REDUNDANT_TOMBSTONES_SQL}))`
|
|
20921
|
+
).run();
|
|
20922
|
+
})();
|
|
20923
|
+
return plan;
|
|
20924
|
+
}
|
|
20925
|
+
var WORKSTREAM_OPS_WHERE = `(key = @ws OR substr(key, 1, length(@ws) + 1) = @ws || '/')`;
|
|
20926
|
+
function listForgetCandidates(db) {
|
|
20927
|
+
const spellings = intentSpellings("workstream.teardown");
|
|
20928
|
+
const names = db.prepare(
|
|
20929
|
+
`SELECT key AS name, MAX(created_at) AS at
|
|
20930
|
+
FROM ops
|
|
20931
|
+
WHERE entity = 'workstream' AND op = 'del'
|
|
20932
|
+
AND intent IN (${spellings.map(() => "?").join(", ")})
|
|
20933
|
+
AND key NOT IN (SELECT name FROM workstreams)
|
|
20934
|
+
GROUP BY key`
|
|
20935
|
+
).all(...spellings);
|
|
20936
|
+
const cost = db.prepare(
|
|
20937
|
+
`SELECT COUNT(*) AS n, COALESCE(SUM(length(payload)), 0) AS b FROM ops WHERE ${WORKSTREAM_OPS_WHERE}`
|
|
20938
|
+
);
|
|
20939
|
+
return names.map((r) => {
|
|
20940
|
+
const c = cost.get({ ws: r.name });
|
|
20941
|
+
return { name: r.name, tornDownAt: r.at, ops: c.n, bytes: c.b };
|
|
20942
|
+
}).sort((a, b) => b.bytes - a.bytes);
|
|
20943
|
+
}
|
|
20944
|
+
function planForget(db, names) {
|
|
20945
|
+
const all = new Map(listForgetCandidates(db).map((c) => [c.name, c]));
|
|
20946
|
+
const live = new Set(
|
|
20947
|
+
db.prepare("SELECT name FROM workstreams").all().map((r) => r.name)
|
|
20948
|
+
);
|
|
20949
|
+
const plan = { candidates: [], refused: [] };
|
|
20950
|
+
for (const name of new Set(names)) {
|
|
20951
|
+
const c = all.get(name);
|
|
20952
|
+
if (c) plan.candidates.push(c);
|
|
20953
|
+
else plan.refused.push({ name, why: live.has(name) ? "live" : "never-torn-down" });
|
|
20954
|
+
}
|
|
20955
|
+
return plan;
|
|
20956
|
+
}
|
|
20957
|
+
function forget(db, plan) {
|
|
20958
|
+
const del = db.prepare(`DELETE FROM ops WHERE ${WORKSTREAM_OPS_WHERE}`);
|
|
20959
|
+
let ops = 0;
|
|
20960
|
+
db.transaction(() => {
|
|
20961
|
+
for (const c of plan.candidates) ops += del.run({ ws: c.name }).changes;
|
|
20962
|
+
})();
|
|
20963
|
+
return { ops };
|
|
20964
|
+
}
|
|
20965
|
+
|
|
20966
|
+
// src/disk-recon.ts
|
|
20967
|
+
init_db();
|
|
20968
|
+
init_workspace();
|
|
20969
|
+
import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
20970
|
+
import { join as join12 } from "path";
|
|
20971
|
+
var STALE_LOCK_REPORT_MS = 60 * 60 * 1e3;
|
|
20972
|
+
function formatBytes(bytes) {
|
|
20973
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
20974
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;
|
|
20975
|
+
if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / 1024 / 1024)}M`;
|
|
20976
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`;
|
|
20977
|
+
}
|
|
20978
|
+
function dirNames(path2) {
|
|
20979
|
+
try {
|
|
20980
|
+
return readdirSync3(path2, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
20981
|
+
} catch {
|
|
20982
|
+
return [];
|
|
20983
|
+
}
|
|
20984
|
+
}
|
|
20985
|
+
function exists(path2) {
|
|
20986
|
+
try {
|
|
20987
|
+
statSync2(path2);
|
|
20988
|
+
return true;
|
|
20989
|
+
} catch {
|
|
20990
|
+
return false;
|
|
20991
|
+
}
|
|
20992
|
+
}
|
|
20993
|
+
function sizeOf(path2) {
|
|
20994
|
+
try {
|
|
20995
|
+
return statSync2(path2).size;
|
|
20996
|
+
} catch {
|
|
20997
|
+
return 0;
|
|
20998
|
+
}
|
|
20999
|
+
}
|
|
21000
|
+
function findMissingWorkspaceDirs(db) {
|
|
21001
|
+
return listWorkspaces2(db).filter((row2) => !exists(row2.path)).map((row2) => ({
|
|
21002
|
+
agentName: row2.agentName,
|
|
21003
|
+
workstreamName: row2.workstreamName,
|
|
21004
|
+
path: row2.path
|
|
21005
|
+
}));
|
|
21006
|
+
}
|
|
21007
|
+
function checkMissingWorkspaceDirs(db) {
|
|
21008
|
+
const missing = findMissingWorkspaceDirs(db);
|
|
21009
|
+
if (missing.length === 0) {
|
|
21010
|
+
return { name: "ws-rows", severity: "ok", detail: "every workspace row has its dir" };
|
|
21011
|
+
}
|
|
21012
|
+
return {
|
|
21013
|
+
name: "ws-rows",
|
|
21014
|
+
severity: "warn",
|
|
21015
|
+
detail: `${missing.length} workspace row(s) point at a path that is gone`,
|
|
21016
|
+
remediation: [
|
|
21017
|
+
"These vcs_workspaces rows name a directory that no longer exists:",
|
|
21018
|
+
...missing.map((m) => ` ${m.workstreamName}/${m.agentName} ${m.path}`),
|
|
21019
|
+
"",
|
|
21020
|
+
"mu reports such a row as a healthy workspace on every read surface, and",
|
|
21021
|
+
"the next send or refresh against it fails inside the VCS backend rather",
|
|
21022
|
+
"than at the row that lied. Unregister them:",
|
|
21023
|
+
...missing.map((m) => ` mu workspace free ${m.agentName} -w ${m.workstreamName}`)
|
|
21024
|
+
]
|
|
21025
|
+
};
|
|
21026
|
+
}
|
|
21027
|
+
function checkWorkspaceOrphanDirs(db) {
|
|
21028
|
+
const orphans = listAllOrphanWorkspaces(db);
|
|
21029
|
+
if (orphans.length === 0) {
|
|
21030
|
+
return { name: "ws-dirs", severity: "ok", detail: "no orphan workspace dirs" };
|
|
21031
|
+
}
|
|
21032
|
+
const stranded = orphans.filter((o) => o.stranded);
|
|
21033
|
+
const strandedNote = stranded.length > 0 ? `, ${stranded.length} stranded` : "";
|
|
21034
|
+
return {
|
|
21035
|
+
name: "ws-dirs",
|
|
21036
|
+
severity: "warn",
|
|
21037
|
+
detail: `${orphans.length} workspace dir(s) with no DB row${strandedNote}`,
|
|
21038
|
+
remediation: [
|
|
21039
|
+
"These directories have no row in vcs_workspaces:",
|
|
21040
|
+
...orphans.map((o) => ` ${o.path}${o.stranded ? " (workstream row also gone)" : ""}`),
|
|
21041
|
+
"",
|
|
21042
|
+
"Each one blocks the next `--workspace` spawn for that agent name. mu will",
|
|
21043
|
+
"not remove them: they may hold uncommitted work, and a stranded dir is the",
|
|
21044
|
+
"only remaining copy of it. Inspect, then remove by hand:",
|
|
21045
|
+
" mu workspace orphans --all",
|
|
21046
|
+
...orphans.slice(0, 3).map((o) => ` rm -rf ${o.path}`)
|
|
21047
|
+
]
|
|
21048
|
+
};
|
|
21049
|
+
}
|
|
21050
|
+
function checkEmptyWorkstreamDirs() {
|
|
21051
|
+
const root = join12(defaultStateDir(), "workspaces");
|
|
21052
|
+
const empty = dirNames(root).filter((name) => dirNames(join12(root, name)).length === 0);
|
|
21053
|
+
if (empty.length === 0) {
|
|
21054
|
+
return { name: "ws-empty", severity: "ok", detail: "no empty workstream dirs" };
|
|
21055
|
+
}
|
|
21056
|
+
return {
|
|
21057
|
+
name: "ws-empty",
|
|
21058
|
+
severity: "ok",
|
|
21059
|
+
detail: `${empty.length} empty workstream dir(s): ${empty.slice(0, 4).join(", ")}`,
|
|
21060
|
+
remediation: [
|
|
21061
|
+
"These per-workstream workspace dirs hold no checkouts:",
|
|
21062
|
+
...empty.map((name) => ` ${join12(root, name)}`),
|
|
21063
|
+
"",
|
|
21064
|
+
"`mu workspace free` removes an agent's dir and its row but not the parent,",
|
|
21065
|
+
"so this is normal residue rather than a fault. Remove if you want the",
|
|
21066
|
+
"state dir to read cleanly:",
|
|
21067
|
+
...empty.slice(0, 3).map((name) => ` rmdir ${join12(root, name)}`)
|
|
21068
|
+
]
|
|
21069
|
+
};
|
|
21070
|
+
}
|
|
21071
|
+
function findStrayDbFiles(stateDir = defaultStateDir()) {
|
|
21072
|
+
const live = /* @__PURE__ */ new Set(["mu.db", "mu.db-wal", "mu.db-shm"]);
|
|
21073
|
+
let entries;
|
|
21074
|
+
try {
|
|
21075
|
+
entries = readdirSync3(stateDir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
21076
|
+
} catch {
|
|
21077
|
+
return [];
|
|
21078
|
+
}
|
|
21079
|
+
return entries.filter((name) => name.startsWith("mu.db") && !live.has(name)).map((name) => ({ name, path: join12(stateDir, name), bytes: sizeOf(join12(stateDir, name)) })).sort((a, b) => b.bytes - a.bytes);
|
|
21080
|
+
}
|
|
21081
|
+
function checkStrayDbFiles() {
|
|
21082
|
+
const stateDir = defaultStateDir();
|
|
21083
|
+
const stray = findStrayDbFiles(stateDir);
|
|
21084
|
+
if (stray.length === 0) {
|
|
21085
|
+
return { name: "db-copies", severity: "ok", detail: "no stray DB copies" };
|
|
21086
|
+
}
|
|
21087
|
+
const total = stray.reduce((n, f) => n + f.bytes, 0);
|
|
21088
|
+
return {
|
|
21089
|
+
name: "db-copies",
|
|
21090
|
+
severity: "warn",
|
|
21091
|
+
detail: `${stray.length} stray DB file(s), ${formatBytes(total)} \u2014 nothing reads these`,
|
|
21092
|
+
remediation: [
|
|
21093
|
+
`Files matching mu.db* in ${stateDir} that are not the live WAL triple:`,
|
|
21094
|
+
...stray.map((f) => ` ${formatBytes(f.bytes).padStart(6)} ${f.name}`),
|
|
21095
|
+
"",
|
|
21096
|
+
`Live DB: ${defaultDbPath()}`,
|
|
21097
|
+
"",
|
|
21098
|
+
"These are hand-made copies and pre-upgrade saves. No mu code path reads",
|
|
21099
|
+
"them and none prunes them. Real disaster recovery is the ops log, so a",
|
|
21100
|
+
"copy is only ever a convenience:",
|
|
21101
|
+
" mu rebuild <file> # DR from the ops log, not from a copy",
|
|
21102
|
+
" mu db backup <file> # a fresh copy, if that is what you wanted"
|
|
21103
|
+
]
|
|
21104
|
+
};
|
|
21105
|
+
}
|
|
21106
|
+
function checkRemovedExportsDir() {
|
|
21107
|
+
const root = join12(defaultStateDir(), "exports");
|
|
21108
|
+
const entries = dirNames(root);
|
|
21109
|
+
if (entries.length === 0) {
|
|
21110
|
+
return { name: "exports", severity: "ok", detail: "no leftover export dirs" };
|
|
21111
|
+
}
|
|
21112
|
+
return {
|
|
21113
|
+
name: "exports",
|
|
21114
|
+
severity: "warn",
|
|
21115
|
+
detail: `${entries.length} export dir(s) from the removed \`mu workstream export\``,
|
|
21116
|
+
remediation: [
|
|
21117
|
+
`${root} holds ${entries.length} directory/ies.`,
|
|
21118
|
+
"",
|
|
21119
|
+
"`mu workstream export` and its markdown bucket were removed in 1.0; this",
|
|
21120
|
+
"is output from a version that still had it. Nothing in mu reads or prunes",
|
|
21121
|
+
"it. Keep anything you still want, then:",
|
|
21122
|
+
` rm -rf ${root}`
|
|
21123
|
+
]
|
|
21124
|
+
};
|
|
21125
|
+
}
|
|
21126
|
+
function checkStaleLocks(now = Date.now()) {
|
|
21127
|
+
const root = join12(defaultStateDir(), "locks");
|
|
21128
|
+
const stale = [];
|
|
21129
|
+
for (const name of dirNames(root)) {
|
|
21130
|
+
try {
|
|
21131
|
+
const ageMs2 = now - statSync2(join12(root, name)).mtimeMs;
|
|
21132
|
+
if (ageMs2 > STALE_LOCK_REPORT_MS) stale.push({ name, ageMs: ageMs2 });
|
|
21133
|
+
} catch {
|
|
21134
|
+
}
|
|
21135
|
+
}
|
|
21136
|
+
if (stale.length === 0) {
|
|
21137
|
+
return { name: "locks", severity: "ok", detail: "no stale lock dirs" };
|
|
21138
|
+
}
|
|
21139
|
+
return {
|
|
21140
|
+
name: "locks",
|
|
21141
|
+
severity: "warn",
|
|
21142
|
+
detail: `${stale.length} lock dir(s) older than 1h`,
|
|
21143
|
+
remediation: [
|
|
21144
|
+
"These advisory-lock dirs outlived the process that took them:",
|
|
21145
|
+
...stale.map((s) => ` ${s.name} (${Math.round(s.ageMs / 6e4)}m old)`),
|
|
21146
|
+
"",
|
|
21147
|
+
"Not a deadlock: withFileLock steals a lock older than 30s. But it only",
|
|
21148
|
+
"steals under contention, so these will sit here forever. They are evidence",
|
|
21149
|
+
"of a spawn or segment flush that died mid-critical-section.",
|
|
21150
|
+
` rm -rf ${root}/<name>`
|
|
21151
|
+
]
|
|
21152
|
+
};
|
|
21153
|
+
}
|
|
21154
|
+
function walkBytes(path2) {
|
|
21155
|
+
let total = 0;
|
|
21156
|
+
let entries;
|
|
21157
|
+
try {
|
|
21158
|
+
entries = readdirSync3(path2, { withFileTypes: true });
|
|
21159
|
+
} catch {
|
|
21160
|
+
return 0;
|
|
21161
|
+
}
|
|
21162
|
+
for (const entry of entries) {
|
|
21163
|
+
const child = join12(path2, entry.name);
|
|
21164
|
+
if (entry.isDirectory()) total += walkBytes(child);
|
|
21165
|
+
else if (entry.isFile()) total += sizeOf(child);
|
|
21166
|
+
}
|
|
21167
|
+
return total;
|
|
21168
|
+
}
|
|
21169
|
+
function measureWorkspaceUsage(db) {
|
|
21170
|
+
const root = join12(defaultStateDir(), "workspaces");
|
|
21171
|
+
const registered = new Set(listWorkspaces2(db).map((w) => w.path));
|
|
21172
|
+
const usage = [];
|
|
21173
|
+
for (const wsName of dirNames(root)) {
|
|
21174
|
+
const wsRoot = join12(root, wsName);
|
|
21175
|
+
for (const agentName of dirNames(wsRoot)) {
|
|
21176
|
+
const path2 = join12(wsRoot, agentName);
|
|
21177
|
+
usage.push({
|
|
21178
|
+
workstreamName: wsName,
|
|
21179
|
+
agentName,
|
|
21180
|
+
path: path2,
|
|
21181
|
+
bytes: walkBytes(path2),
|
|
21182
|
+
orphan: !registered.has(path2)
|
|
21183
|
+
});
|
|
21184
|
+
}
|
|
21185
|
+
}
|
|
21186
|
+
return usage.sort((a, b) => b.bytes - a.bytes);
|
|
21187
|
+
}
|
|
21188
|
+
function checkDiskRecon(db) {
|
|
21189
|
+
return [
|
|
21190
|
+
checkMissingWorkspaceDirs(db),
|
|
21191
|
+
checkWorkspaceOrphanDirs(db),
|
|
21192
|
+
checkEmptyWorkstreamDirs(),
|
|
21193
|
+
checkStrayDbFiles(),
|
|
21194
|
+
checkRemovedExportsDir(),
|
|
21195
|
+
checkStaleLocks()
|
|
21196
|
+
];
|
|
21197
|
+
}
|
|
21198
|
+
|
|
21199
|
+
// src/cli/db.ts
|
|
21200
|
+
init_drift();
|
|
20891
21201
|
init_output();
|
|
20892
21202
|
|
|
20893
21203
|
// src/cli/handle.ts
|
|
@@ -21695,6 +22005,110 @@ function cmdDbBackup(db, file, opts = {}) {
|
|
|
21695
22005
|
console.log(`Backed up to ${pc.bold(target)}`);
|
|
21696
22006
|
printNextSteps(nextSteps3);
|
|
21697
22007
|
}
|
|
22008
|
+
function backupBeside(db, verb) {
|
|
22009
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
22010
|
+
const target = `${db.name}.pre-${verb}-${ts}`;
|
|
22011
|
+
db.prepare("VACUUM INTO ?").run(target);
|
|
22012
|
+
return target;
|
|
22013
|
+
}
|
|
22014
|
+
function vacuumAndCheck(db, backup) {
|
|
22015
|
+
const before = statSync5(db.name).size;
|
|
22016
|
+
db.exec("VACUUM");
|
|
22017
|
+
const after = statSync5(db.name).size;
|
|
22018
|
+
const report = checkDrift(db);
|
|
22019
|
+
if (!report.clean) {
|
|
22020
|
+
console.error(
|
|
22021
|
+
pc.red(`drift after the rewrite (${report.totalDrift}); the backup is ${backup}`)
|
|
22022
|
+
);
|
|
22023
|
+
throw new DriftDetectedError(report.totalDrift, report.records);
|
|
22024
|
+
}
|
|
22025
|
+
return { before, after };
|
|
22026
|
+
}
|
|
22027
|
+
function cmdDbCompact(db, opts = {}) {
|
|
22028
|
+
const plan = planCompact(db);
|
|
22029
|
+
if (opts.yes !== true || plan.tombstones === 0) {
|
|
22030
|
+
const nextSteps3 = plan.tombstones === 0 ? [] : [{ intent: "Apply it", command: "mu db compact --yes" }];
|
|
22031
|
+
if (opts.json === true) {
|
|
22032
|
+
emitJson({ dryRun: true, ...plan, nextSteps: nextSteps3 });
|
|
22033
|
+
return;
|
|
22034
|
+
}
|
|
22035
|
+
console.log(
|
|
22036
|
+
plan.tombstones === 0 ? pc.dim("nothing to compact: no redundant note tombstones") : `${plan.tombstones} note tombstones repeat a put already in the log (${formatBytes(plan.bytes)}). Dry run; nothing changed.`
|
|
22037
|
+
);
|
|
22038
|
+
printNextSteps(nextSteps3);
|
|
22039
|
+
return;
|
|
22040
|
+
}
|
|
22041
|
+
const backup = backupBeside(db, "compact");
|
|
22042
|
+
compact(db);
|
|
22043
|
+
const size = vacuumAndCheck(db, backup);
|
|
22044
|
+
if (opts.json === true) {
|
|
22045
|
+
emitJson({ dryRun: false, ...plan, backup, ...size });
|
|
22046
|
+
return;
|
|
22047
|
+
}
|
|
22048
|
+
console.log(
|
|
22049
|
+
`Compacted ${plan.tombstones} note tombstones: ${formatBytes(size.before)} \u2192 ${formatBytes(size.after)}. Drift check clean.`
|
|
22050
|
+
);
|
|
22051
|
+
console.log(pc.dim(`backup: ${backup}`));
|
|
22052
|
+
}
|
|
22053
|
+
function printForgetTable(plan) {
|
|
22054
|
+
const t = muTable({ head: ["workstream", "torn down", "ops", "size"].map((h) => pc.bold(h)) });
|
|
22055
|
+
for (const c of plan.candidates)
|
|
22056
|
+
t.push([c.name, c.tornDownAt.slice(0, 10), String(c.ops), formatBytes(c.bytes)]);
|
|
22057
|
+
console.log(t.toString());
|
|
22058
|
+
}
|
|
22059
|
+
function cmdDbForget(db, names, opts = {}) {
|
|
22060
|
+
const plan = planForget(db, names);
|
|
22061
|
+
if (plan.refused.length > 0) {
|
|
22062
|
+
const why = plan.refused.map((r2) => `${r2.name} (${r2.why === "live" ? "is live" : "was never torn down"})`).join(", ");
|
|
22063
|
+
throw new UsageError(`cannot forget: ${why}. Only torn-down workstreams can be forgotten.`);
|
|
22064
|
+
}
|
|
22065
|
+
const ops = plan.candidates.reduce((n, c) => n + c.ops, 0);
|
|
22066
|
+
const bytes = plan.candidates.reduce((n, c) => n + c.bytes, 0);
|
|
22067
|
+
if (opts.yes !== true) {
|
|
22068
|
+
const nextSteps3 = [
|
|
22069
|
+
{ intent: "Forget them (NOT undoable)", command: `mu db forget ${names.join(" ")} --yes` }
|
|
22070
|
+
];
|
|
22071
|
+
if (opts.json === true) {
|
|
22072
|
+
emitJson({ dryRun: true, candidates: plan.candidates, ops, bytes, nextSteps: nextSteps3 });
|
|
22073
|
+
return;
|
|
22074
|
+
}
|
|
22075
|
+
printForgetTable(plan);
|
|
22076
|
+
console.log(
|
|
22077
|
+
`${ops} ops (${formatBytes(bytes)} of payload). Forgetting deletes their history: mu undo can no longer restore these teardowns. Dry run; nothing changed.`
|
|
22078
|
+
);
|
|
22079
|
+
printNextSteps(nextSteps3);
|
|
22080
|
+
return;
|
|
22081
|
+
}
|
|
22082
|
+
const backup = backupBeside(db, "forget");
|
|
22083
|
+
const r = forget(db, plan);
|
|
22084
|
+
const size = vacuumAndCheck(db, backup);
|
|
22085
|
+
if (opts.json === true) {
|
|
22086
|
+
emitJson({
|
|
22087
|
+
dryRun: false,
|
|
22088
|
+
forgotten: plan.candidates.map((c) => c.name),
|
|
22089
|
+
ops: r.ops,
|
|
22090
|
+
backup,
|
|
22091
|
+
...size
|
|
22092
|
+
});
|
|
22093
|
+
return;
|
|
22094
|
+
}
|
|
22095
|
+
console.log(
|
|
22096
|
+
`Forgot ${plan.candidates.length} workstream(s), ${r.ops} ops: ${formatBytes(size.before)} \u2192 ${formatBytes(size.after)}. Drift check clean.`
|
|
22097
|
+
);
|
|
22098
|
+
console.log(pc.dim(`backup (the only way back): ${backup}`));
|
|
22099
|
+
}
|
|
22100
|
+
function forgetHint(db) {
|
|
22101
|
+
const all = listForgetCandidates(db);
|
|
22102
|
+
const bytes = all.reduce((n, c) => n + c.bytes, 0);
|
|
22103
|
+
if (bytes < FORGET_HINT_MIN_BYTES) return void 0;
|
|
22104
|
+
return {
|
|
22105
|
+
total: all.length,
|
|
22106
|
+
ops: all.reduce((n, c) => n + c.ops, 0),
|
|
22107
|
+
bytes,
|
|
22108
|
+
top: all.slice(0, 3).map((c) => c.name)
|
|
22109
|
+
};
|
|
22110
|
+
}
|
|
22111
|
+
var FORGET_HINT_MIN_BYTES = 5 * 1024 * 1024;
|
|
21698
22112
|
function wireDbCommands(program) {
|
|
21699
22113
|
const dbCmd = program.command("db").description("Whole-DB file commands");
|
|
21700
22114
|
dbCmd.command("backup <file>").description(
|
|
@@ -21703,247 +22117,24 @@ function wireDbCommands(program) {
|
|
|
21703
22117
|
const opts = this.opts();
|
|
21704
22118
|
return handle(async (db) => cmdDbBackup(db, file, opts), this)();
|
|
21705
22119
|
});
|
|
22120
|
+
dbCmd.command("compact").description(
|
|
22121
|
+
"Shrink mu.db: blank note tombstones that repeat a put already in the log, then VACUUM. Dry run without --yes; --yes backs up beside the DB first and runs the drift check after. Local only; sync segments are untouched."
|
|
22122
|
+
).option("--yes", "apply (default: dry run)").option(...JSON_OPT).action(function() {
|
|
22123
|
+
const opts = this.opts();
|
|
22124
|
+
return handle(async (db) => cmdDbCompact(db, opts), this)();
|
|
22125
|
+
});
|
|
22126
|
+
dbCmd.command("forget <workstreams...>").description(
|
|
22127
|
+
"Delete every op of torn-down workstreams to shrink mu.db. NOT undoable: mu undo can no longer restore them. Dry run without --yes; --yes backs up beside the DB first, VACUUMs, and runs the drift check. List candidates: mu workstream list --torn-down."
|
|
22128
|
+
).option("--yes", "apply (default: dry run)").option(...JSON_OPT).action(function(names) {
|
|
22129
|
+
const opts = this.opts();
|
|
22130
|
+
return handle(async (db) => cmdDbForget(db, names, opts), this)();
|
|
22131
|
+
});
|
|
21706
22132
|
}
|
|
21707
22133
|
|
|
21708
22134
|
// src/cli/doctor.ts
|
|
21709
22135
|
init_agents();
|
|
21710
22136
|
import { dirname as dirname14 } from "path";
|
|
21711
22137
|
init_db();
|
|
21712
|
-
|
|
21713
|
-
// src/disk-recon.ts
|
|
21714
|
-
init_db();
|
|
21715
|
-
init_workspace();
|
|
21716
|
-
import { readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
21717
|
-
import { join as join13 } from "path";
|
|
21718
|
-
var STALE_LOCK_REPORT_MS = 60 * 60 * 1e3;
|
|
21719
|
-
function formatBytes(bytes) {
|
|
21720
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
21721
|
-
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;
|
|
21722
|
-
if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / 1024 / 1024)}M`;
|
|
21723
|
-
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`;
|
|
21724
|
-
}
|
|
21725
|
-
function dirNames(path2) {
|
|
21726
|
-
try {
|
|
21727
|
-
return readdirSync4(path2, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
21728
|
-
} catch {
|
|
21729
|
-
return [];
|
|
21730
|
-
}
|
|
21731
|
-
}
|
|
21732
|
-
function exists(path2) {
|
|
21733
|
-
try {
|
|
21734
|
-
statSync4(path2);
|
|
21735
|
-
return true;
|
|
21736
|
-
} catch {
|
|
21737
|
-
return false;
|
|
21738
|
-
}
|
|
21739
|
-
}
|
|
21740
|
-
function sizeOf(path2) {
|
|
21741
|
-
try {
|
|
21742
|
-
return statSync4(path2).size;
|
|
21743
|
-
} catch {
|
|
21744
|
-
return 0;
|
|
21745
|
-
}
|
|
21746
|
-
}
|
|
21747
|
-
function findMissingWorkspaceDirs(db) {
|
|
21748
|
-
return listWorkspaces2(db).filter((row2) => !exists(row2.path)).map((row2) => ({
|
|
21749
|
-
agentName: row2.agentName,
|
|
21750
|
-
workstreamName: row2.workstreamName,
|
|
21751
|
-
path: row2.path
|
|
21752
|
-
}));
|
|
21753
|
-
}
|
|
21754
|
-
function checkMissingWorkspaceDirs(db) {
|
|
21755
|
-
const missing = findMissingWorkspaceDirs(db);
|
|
21756
|
-
if (missing.length === 0) {
|
|
21757
|
-
return { name: "ws-rows", severity: "ok", detail: "every workspace row has its dir" };
|
|
21758
|
-
}
|
|
21759
|
-
return {
|
|
21760
|
-
name: "ws-rows",
|
|
21761
|
-
severity: "warn",
|
|
21762
|
-
detail: `${missing.length} workspace row(s) point at a path that is gone`,
|
|
21763
|
-
remediation: [
|
|
21764
|
-
"These vcs_workspaces rows name a directory that no longer exists:",
|
|
21765
|
-
...missing.map((m) => ` ${m.workstreamName}/${m.agentName} ${m.path}`),
|
|
21766
|
-
"",
|
|
21767
|
-
"mu reports such a row as a healthy workspace on every read surface, and",
|
|
21768
|
-
"the next send or refresh against it fails inside the VCS backend rather",
|
|
21769
|
-
"than at the row that lied. Unregister them:",
|
|
21770
|
-
...missing.map((m) => ` mu workspace free ${m.agentName} -w ${m.workstreamName}`)
|
|
21771
|
-
]
|
|
21772
|
-
};
|
|
21773
|
-
}
|
|
21774
|
-
function checkWorkspaceOrphanDirs(db) {
|
|
21775
|
-
const orphans = listAllOrphanWorkspaces(db);
|
|
21776
|
-
if (orphans.length === 0) {
|
|
21777
|
-
return { name: "ws-dirs", severity: "ok", detail: "no orphan workspace dirs" };
|
|
21778
|
-
}
|
|
21779
|
-
const stranded = orphans.filter((o) => o.stranded);
|
|
21780
|
-
const strandedNote = stranded.length > 0 ? `, ${stranded.length} stranded` : "";
|
|
21781
|
-
return {
|
|
21782
|
-
name: "ws-dirs",
|
|
21783
|
-
severity: "warn",
|
|
21784
|
-
detail: `${orphans.length} workspace dir(s) with no DB row${strandedNote}`,
|
|
21785
|
-
remediation: [
|
|
21786
|
-
"These directories have no row in vcs_workspaces:",
|
|
21787
|
-
...orphans.map((o) => ` ${o.path}${o.stranded ? " (workstream row also gone)" : ""}`),
|
|
21788
|
-
"",
|
|
21789
|
-
"Each one blocks the next `--workspace` spawn for that agent name. mu will",
|
|
21790
|
-
"not remove them: they may hold uncommitted work, and a stranded dir is the",
|
|
21791
|
-
"only remaining copy of it. Inspect, then remove by hand:",
|
|
21792
|
-
" mu workspace orphans --all",
|
|
21793
|
-
...orphans.slice(0, 3).map((o) => ` rm -rf ${o.path}`)
|
|
21794
|
-
]
|
|
21795
|
-
};
|
|
21796
|
-
}
|
|
21797
|
-
function checkEmptyWorkstreamDirs() {
|
|
21798
|
-
const root = join13(defaultStateDir(), "workspaces");
|
|
21799
|
-
const empty = dirNames(root).filter((name) => dirNames(join13(root, name)).length === 0);
|
|
21800
|
-
if (empty.length === 0) {
|
|
21801
|
-
return { name: "ws-empty", severity: "ok", detail: "no empty workstream dirs" };
|
|
21802
|
-
}
|
|
21803
|
-
return {
|
|
21804
|
-
name: "ws-empty",
|
|
21805
|
-
severity: "ok",
|
|
21806
|
-
detail: `${empty.length} empty workstream dir(s): ${empty.slice(0, 4).join(", ")}`,
|
|
21807
|
-
remediation: [
|
|
21808
|
-
"These per-workstream workspace dirs hold no checkouts:",
|
|
21809
|
-
...empty.map((name) => ` ${join13(root, name)}`),
|
|
21810
|
-
"",
|
|
21811
|
-
"`mu workspace free` removes an agent's dir and its row but not the parent,",
|
|
21812
|
-
"so this is normal residue rather than a fault. Remove if you want the",
|
|
21813
|
-
"state dir to read cleanly:",
|
|
21814
|
-
...empty.slice(0, 3).map((name) => ` rmdir ${join13(root, name)}`)
|
|
21815
|
-
]
|
|
21816
|
-
};
|
|
21817
|
-
}
|
|
21818
|
-
function findStrayDbFiles(stateDir = defaultStateDir()) {
|
|
21819
|
-
const live = /* @__PURE__ */ new Set(["mu.db", "mu.db-wal", "mu.db-shm"]);
|
|
21820
|
-
let entries;
|
|
21821
|
-
try {
|
|
21822
|
-
entries = readdirSync4(stateDir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
21823
|
-
} catch {
|
|
21824
|
-
return [];
|
|
21825
|
-
}
|
|
21826
|
-
return entries.filter((name) => name.startsWith("mu.db") && !live.has(name)).map((name) => ({ name, path: join13(stateDir, name), bytes: sizeOf(join13(stateDir, name)) })).sort((a, b) => b.bytes - a.bytes);
|
|
21827
|
-
}
|
|
21828
|
-
function checkStrayDbFiles() {
|
|
21829
|
-
const stateDir = defaultStateDir();
|
|
21830
|
-
const stray = findStrayDbFiles(stateDir);
|
|
21831
|
-
if (stray.length === 0) {
|
|
21832
|
-
return { name: "db-copies", severity: "ok", detail: "no stray DB copies" };
|
|
21833
|
-
}
|
|
21834
|
-
const total = stray.reduce((n, f) => n + f.bytes, 0);
|
|
21835
|
-
return {
|
|
21836
|
-
name: "db-copies",
|
|
21837
|
-
severity: "warn",
|
|
21838
|
-
detail: `${stray.length} stray DB file(s), ${formatBytes(total)} \u2014 nothing reads these`,
|
|
21839
|
-
remediation: [
|
|
21840
|
-
`Files matching mu.db* in ${stateDir} that are not the live WAL triple:`,
|
|
21841
|
-
...stray.map((f) => ` ${formatBytes(f.bytes).padStart(6)} ${f.name}`),
|
|
21842
|
-
"",
|
|
21843
|
-
`Live DB: ${defaultDbPath()}`,
|
|
21844
|
-
"",
|
|
21845
|
-
"These are hand-made copies and pre-upgrade saves. No mu code path reads",
|
|
21846
|
-
"them and none prunes them. Real disaster recovery is the ops log, so a",
|
|
21847
|
-
"copy is only ever a convenience:",
|
|
21848
|
-
" mu rebuild <file> # DR from the ops log, not from a copy",
|
|
21849
|
-
" mu db backup <file> # a fresh copy, if that is what you wanted"
|
|
21850
|
-
]
|
|
21851
|
-
};
|
|
21852
|
-
}
|
|
21853
|
-
function checkRemovedExportsDir() {
|
|
21854
|
-
const root = join13(defaultStateDir(), "exports");
|
|
21855
|
-
const entries = dirNames(root);
|
|
21856
|
-
if (entries.length === 0) {
|
|
21857
|
-
return { name: "exports", severity: "ok", detail: "no leftover export dirs" };
|
|
21858
|
-
}
|
|
21859
|
-
return {
|
|
21860
|
-
name: "exports",
|
|
21861
|
-
severity: "warn",
|
|
21862
|
-
detail: `${entries.length} export dir(s) from the removed \`mu workstream export\``,
|
|
21863
|
-
remediation: [
|
|
21864
|
-
`${root} holds ${entries.length} directory/ies.`,
|
|
21865
|
-
"",
|
|
21866
|
-
"`mu workstream export` and its markdown bucket were removed in 1.0; this",
|
|
21867
|
-
"is output from a version that still had it. Nothing in mu reads or prunes",
|
|
21868
|
-
"it. Keep anything you still want, then:",
|
|
21869
|
-
` rm -rf ${root}`
|
|
21870
|
-
]
|
|
21871
|
-
};
|
|
21872
|
-
}
|
|
21873
|
-
function checkStaleLocks(now = Date.now()) {
|
|
21874
|
-
const root = join13(defaultStateDir(), "locks");
|
|
21875
|
-
const stale = [];
|
|
21876
|
-
for (const name of dirNames(root)) {
|
|
21877
|
-
try {
|
|
21878
|
-
const ageMs2 = now - statSync4(join13(root, name)).mtimeMs;
|
|
21879
|
-
if (ageMs2 > STALE_LOCK_REPORT_MS) stale.push({ name, ageMs: ageMs2 });
|
|
21880
|
-
} catch {
|
|
21881
|
-
}
|
|
21882
|
-
}
|
|
21883
|
-
if (stale.length === 0) {
|
|
21884
|
-
return { name: "locks", severity: "ok", detail: "no stale lock dirs" };
|
|
21885
|
-
}
|
|
21886
|
-
return {
|
|
21887
|
-
name: "locks",
|
|
21888
|
-
severity: "warn",
|
|
21889
|
-
detail: `${stale.length} lock dir(s) older than 1h`,
|
|
21890
|
-
remediation: [
|
|
21891
|
-
"These advisory-lock dirs outlived the process that took them:",
|
|
21892
|
-
...stale.map((s) => ` ${s.name} (${Math.round(s.ageMs / 6e4)}m old)`),
|
|
21893
|
-
"",
|
|
21894
|
-
"Not a deadlock: withFileLock steals a lock older than 30s. But it only",
|
|
21895
|
-
"steals under contention, so these will sit here forever. They are evidence",
|
|
21896
|
-
"of a spawn or segment flush that died mid-critical-section.",
|
|
21897
|
-
` rm -rf ${root}/<name>`
|
|
21898
|
-
]
|
|
21899
|
-
};
|
|
21900
|
-
}
|
|
21901
|
-
function walkBytes(path2) {
|
|
21902
|
-
let total = 0;
|
|
21903
|
-
let entries;
|
|
21904
|
-
try {
|
|
21905
|
-
entries = readdirSync4(path2, { withFileTypes: true });
|
|
21906
|
-
} catch {
|
|
21907
|
-
return 0;
|
|
21908
|
-
}
|
|
21909
|
-
for (const entry of entries) {
|
|
21910
|
-
const child = join13(path2, entry.name);
|
|
21911
|
-
if (entry.isDirectory()) total += walkBytes(child);
|
|
21912
|
-
else if (entry.isFile()) total += sizeOf(child);
|
|
21913
|
-
}
|
|
21914
|
-
return total;
|
|
21915
|
-
}
|
|
21916
|
-
function measureWorkspaceUsage(db) {
|
|
21917
|
-
const root = join13(defaultStateDir(), "workspaces");
|
|
21918
|
-
const registered = new Set(listWorkspaces2(db).map((w) => w.path));
|
|
21919
|
-
const usage = [];
|
|
21920
|
-
for (const wsName of dirNames(root)) {
|
|
21921
|
-
const wsRoot = join13(root, wsName);
|
|
21922
|
-
for (const agentName of dirNames(wsRoot)) {
|
|
21923
|
-
const path2 = join13(wsRoot, agentName);
|
|
21924
|
-
usage.push({
|
|
21925
|
-
workstreamName: wsName,
|
|
21926
|
-
agentName,
|
|
21927
|
-
path: path2,
|
|
21928
|
-
bytes: walkBytes(path2),
|
|
21929
|
-
orphan: !registered.has(path2)
|
|
21930
|
-
});
|
|
21931
|
-
}
|
|
21932
|
-
}
|
|
21933
|
-
return usage.sort((a, b) => b.bytes - a.bytes);
|
|
21934
|
-
}
|
|
21935
|
-
function checkDiskRecon(db) {
|
|
21936
|
-
return [
|
|
21937
|
-
checkMissingWorkspaceDirs(db),
|
|
21938
|
-
checkWorkspaceOrphanDirs(db),
|
|
21939
|
-
checkEmptyWorkstreamDirs(),
|
|
21940
|
-
checkStrayDbFiles(),
|
|
21941
|
-
checkRemovedExportsDir(),
|
|
21942
|
-
checkStaleLocks()
|
|
21943
|
-
];
|
|
21944
|
-
}
|
|
21945
|
-
|
|
21946
|
-
// src/cli/doctor.ts
|
|
21947
22138
|
init_doctor_summary();
|
|
21948
22139
|
|
|
21949
22140
|
// src/dormant.ts
|
|
@@ -22272,12 +22463,37 @@ state (workstream=${ws})`));
|
|
|
22272
22463
|
throw new DriftDetectedError(cheap.totalUnexplained, []);
|
|
22273
22464
|
}
|
|
22274
22465
|
}
|
|
22466
|
+
const size = opsSizeHint(db);
|
|
22467
|
+
if (size) {
|
|
22468
|
+
console.log(` size : ${pc.yellow("hint")} ${size.line}`);
|
|
22469
|
+
for (const c of size.commands) console.log(pc.dim(` ${c}`));
|
|
22470
|
+
}
|
|
22275
22471
|
if (sawHazard) {
|
|
22276
22472
|
console.log(
|
|
22277
22473
|
pc.dim("\nSee the fleet / disk sections above: at least one finding needs attention.")
|
|
22278
22474
|
);
|
|
22279
22475
|
}
|
|
22280
22476
|
}
|
|
22477
|
+
function opsSizeHint(db) {
|
|
22478
|
+
const forgettable = forgetHint(db);
|
|
22479
|
+
const tomb = planCompact(db);
|
|
22480
|
+
const parts = [];
|
|
22481
|
+
const commands = [];
|
|
22482
|
+
if (forgettable) {
|
|
22483
|
+
parts.push(
|
|
22484
|
+
`${forgettable.total} torn-down workstreams hold ${forgettable.ops} ops (${formatBytes(forgettable.bytes)}); largest: ${forgettable.top.join(", ")}`
|
|
22485
|
+
);
|
|
22486
|
+
commands.push(
|
|
22487
|
+
"see them: mu workstream list --torn-down",
|
|
22488
|
+
`forget some: mu db forget ${forgettable.top.join(" ")}`
|
|
22489
|
+
);
|
|
22490
|
+
}
|
|
22491
|
+
if (tomb.bytes >= 1024 * 1024) {
|
|
22492
|
+
parts.push(`${tomb.tombstones} redundant note tombstones (${formatBytes(tomb.bytes)})`);
|
|
22493
|
+
commands.push("compact: mu db compact");
|
|
22494
|
+
}
|
|
22495
|
+
return parts.length === 0 ? void 0 : { line: parts.join("; "), commands };
|
|
22496
|
+
}
|
|
22281
22497
|
async function cmdDoctorJson(db, opts = {}) {
|
|
22282
22498
|
const health = await muxHealth();
|
|
22283
22499
|
const muExt = ctlExtensionDoctorCheck();
|
|
@@ -22419,6 +22635,7 @@ async function cmdDoctorJson(db, opts = {}) {
|
|
|
22419
22635
|
disk,
|
|
22420
22636
|
workspaceUsage,
|
|
22421
22637
|
drift,
|
|
22638
|
+
size: opsSizeHint(db) ?? null,
|
|
22422
22639
|
remediation: drift.ok === true ? [] : driftRemediation()
|
|
22423
22640
|
});
|
|
22424
22641
|
if (driftFailure !== null) throw driftFailure;
|