feat(runs): read-only runs and releases reader module (#1545, row 56)

New packages/runs reads run records, result.json, the release pointer
and the activation log without writing, pruning or following a link out
of the data root. mosaic-task.mjs list and show use it, so a malformed
result.json or a run that is a regular file no longer crashes them.
Deliberate deltas are listed in the README.

Rocko built it. Round 1 (ed3c5392) was approved with notes by Darkwing
(27115) and Filbert (27116); round 2 (2727198f, tests and wording only)
was approved by Darkwing (27123) and Filbert (27124). Sage's gate on
c9a25a47 plus the candidate: runs 41/0, queue 148/0, webui 22/0,
conversation 182/0 (181/1 in the full run on the K12 cgroup timing
test under load, 182/0 alone), control-board 124/0, every
scripts/test-*.sh 0 failed (task 98/0 with Docker, 26/0 without).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-10 12:22:37 -05:00
co-authored by Claude Opus 5.5
parent 61ec09822f
commit 71d874764a
16 changed files with 1083 additions and 27 deletions
+18 -27
View File
@@ -35,6 +35,7 @@ import { randomBytes } from "node:crypto";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { BusinessError, validateRoleDocument } from "../packages/business/src/index.mjs";
import { RunsError, isRunId, listRunRecords, readRunRecord } from "../packages/runs/src/index.mjs";
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const RUNS_DIRNAME = "runs";
@@ -457,53 +458,43 @@ function runTask(taskFile, options = {}) {
process.exit(status === "succeeded" ? 0 : 1);
}
function readRuns(read) {
try {
return read();
} catch (error) {
if (error instanceof RunsError) fail(error.exitCode, error.message);
throw error;
}
}
function listRuns() {
const resolved = loadConfig();
const root = runsRoot(resolved);
let entries = [];
try {
entries = fs.readdirSync(root).filter((name) => name.startsWith("r-")).sort();
} catch {
// No runs yet.
}
for (const runId of entries) {
for (const { runId, result } of readRuns(() => listRunRecords(resolved.dataRoot))) {
// An incomplete or unreadable run record (result null) reports as unknown.
let status = "unknown";
let taskId = "-";
let workspace = "-";
let session = "-";
try {
const result = JSON.parse(fs.readFileSync(path.join(root, runId, "result.json"), "utf8"));
if (result) {
status = result.status;
taskId = result.taskId;
workspace = result.workspace ?? "-";
session = result.session ?? "-";
} catch {
// Incomplete run record; report as unknown.
}
process.stdout.write(`${runId} ${status.padEnd(9)} task=${taskId.padEnd(18)} ws=${String(workspace).padEnd(10)} session=${session}\n`);
}
}
function showRun(runId) {
if (!/^r-[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(runId)) {
if (!isRunId(runId)) {
fail(4, `invalid run id: ${JSON.stringify(runId)} (expected r-<id>)`);
}
const resolved = loadConfig();
const dir = path.join(runsRoot(resolved), runId);
if (!fs.existsSync(dir)) {
const record = readRuns(() => readRunRecord(resolved.dataRoot, runId));
if (record === null) {
fail(4, `run not found: ${runId} (under ${runsRoot(resolved)})`);
}
const read = (name) => {
try {
return JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
} catch {
return null;
}
};
const result = read("result.json");
const task = read("task.json");
const mission = read("mission.json");
const { result, task, mission } = record;
process.stdout.write(`run: ${runId}\n`);
if (result) {
@@ -529,7 +520,7 @@ function showRun(runId) {
}
if (task) process.stdout.write(`task snapshot: ${"task.json"} present\n`);
if (mission) process.stdout.write(`mission snapshot: ${mission.id} - ${mission.objective}\n`);
process.stdout.write(`artifacts: ${fs.readdirSync(dir).map((f) => `${f}`).join(", ")}\n`);
process.stdout.write(`artifacts: ${record.artifacts.join(", ")}\n`);
process.exit(0);
}