Files
stack/packages/runs
jason.woltjeandClaude Opus 5.5 71d874764a 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]>
2026-10-10 12:22:37 -05:00
..

Runs

Read-only readers for the run records under <dataRoot>/runs/ and the release state under <dataRoot>/state/ (#1545). scripts/mosaic-task.mjs list and show read through it, and so will the console's runs view. This README covers what the code does and the limits it accepts.

node --test packages/runs/tests/

What it reads

Export Returns
listRunIds(dataRoot) every name under runs/ that starts with r-, sorted, which is oldest first
listRunRecords(dataRoot) [{ runId, result }] for those names; result is null for an incomplete or unreadable record
readRunRecord(dataRoot, runId) { runId, result, task, mission, artifacts }, or null when the run doesn't exist or isn't a directory
readRunDocument(dataRoot, runId, name) result.json, task.json or mission.json from one run, or null
readActivePointer(dataRoot) state/active.json, or null when no release has been activated
readActivationLog(dataRoot, { last }) { entries, malformed } from state/activation-log.jsonl, oldest first
isRunId(value), RUN_ID_PATTERN the run id shape mosaic-task.mjs checks: r- then 1 to 64 of [A-Za-z0-9._-], starting with a letter or digit

The caller passes dataRoot; this package doesn't read the system config. artifacts are names in directory order, as show has always printed them.

Limits

  • Nothing writes. No reader creates, changes, prunes or locks anything. Run records stay write-once evidence; pruning stays in mosaic-task.mjs prune.

  • Nothing follows a link out of the data root. The configured data root is trusted as given, even when it is itself a link. Every path below it is resolved, and one that resolves outside it is refused or read as unreadable:

    • runs/ or state/ resolving outside refuses with a RunsError;
    • a run directory resolving outside: readRunRecord refuses, and listRunRecords gives that run a null result;
    • a run document resolving outside reads as null;
    • active.json or activation-log.jsonl resolving outside refuses.

    Only a path that exists can resolve. A link with nothing behind it reads as missing wherever it points. A state/ link out of the data root to a path that doesn't exist reads as no release and an empty log, and a runs/ link like that lists nothing. A link that stays inside the data root is followed.

  • Run documents are JSON objects or null. A document that is missing, unreadable, not JSON, or JSON but not an object (null, 5, []) reads as null. Run records are never validated beyond that, because older records carry older shapes.

  • The release pointer is strict. active.json must be the version 1 shape scripts/release.sh writes: exactly pointerVersion 1 and non-empty strings release, imageTag and activatedAt. Anything else refuses with exit code 2 rather than being guessed at.

  • The activation log is read per line. One bad line never refuses the log. An entry needs string at, event, release and imageTag, an optional string note, and no other keys. A line that isn't JSON, or is JSON with another shape, is counted in malformed and skipped. That is stricter than release.sh rollback, which skips only lines that aren't JSON and would act on an entry with an extra key or a non-string field. Blank lines aren't counted. last must be a positive integer and keeps the newest entries.

  • Errors. Every refusal is a RunsError with exitCode 2 (invalid data) or 4 (a file or environment problem), matching mosaic-task.mjs. A missing data root, runs/ or state/ file is not an error. A path that can't be resolved for another reason (a link loop, a permission error) or a directory that can't be read refuses instead of reading as empty.

How mosaic-task.mjs uses it

list and show print exactly what they printed before this package existed, for any data root with no link out of it, no run document that is JSON but not an object, and no unreadable directory. agents/rocko/work/queue-56/byte-check.sh checks that byte for byte against a seeded data root. Where those conditions don't hold, the output changes on purpose:

Case Before Now
run directory or result.json linked out read through the link list: unknown; show: refuses (run directory) or result.json: (missing or unreadable)
runs/ linked out read through the link list and show refuse, exit 4
result.json is 5 or [] list crashed; show printed undefined fields unknown; (missing or unreadable)
task.json or mission.json is JSON but not an object show printed its snapshot line snapshot line omitted
run is a regular file show crashed run not found, exit 4
runs/ unreadable list printed nothing refuses, exit 4
run directory unreadable (mode 000) show printed two lines, then crashed, exit 1 refuses with EACCES, exit 4
link loop or a permission error resolving a run run not found refuses with the error code, exit 4

agents/rocko/work/queue-56/delta-check.sh prints the before and after for each case.