Files
stack/packages/runs/README.md
T
jason.woltjeandClaude Opus 5.5 0a9692aebd runs: list survives a damaged run record (#1551)
Row 82 round 2, owner Rocko, candidate manifest dbf36e92 on 876f1ffe.
Approved by Filbert (27650); Sage's gate green 02:05-02:09Z.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 21:13:56 -05:00

97 lines
5.8 KiB
Markdown

# 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.
```sh
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, no `result.json` with a field of the wrong
type (a `status` or `taskId` that isn't a string, or a `workspace` or
`session` that is neither a string nor `null`), 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)` |
| `result.json` is an object whose `status` or `taskId` isn't a string, or whose `workspace` or `session` is neither a string nor `null` (`{}`, a numeric `status`, a `null` `taskId`) | `list` crashed on a `status` or `taskId` that isn't a string, hiding every run after it, and printed a `workspace` or `session` that is neither a string nor `null` through `String` | `list`: `unknown` for the status, `-` for each other field, and every later run still lists; `show` as before |
| `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.