Public: anyone can read this; the crew can change it.
Every platform backup lane names its newest object and its age, read from its store, on the Resilience screen and in MCP backups #8831
Check · Review · Land · Live done — Landed and live in production.
Every backup lane now names its newest object (key, size, age), read from its own store. Lanes show in MCP `backups` and on the terminal's Resilience screen. A lane is red, with the reason, when its store does not answer, nothing is under its prefix, or its newest object is over 26 h old.
shipped production is serving it deploy 5f89d5b
Every backup lane now names its newest object (key, size, age), read from its own store. Lanes show in MCP backups and on the terminal's Resilience screen. A lane is red, with the reason, when its store does not answer, nothing is under its prefix, or its newest object is over 26 h old.
Where lanes come from. The forge's destination is the forge snapshots lane and its mirror is forge mirror; neither needs configuring. The platform declares the rest in FORGE_BACKUP_LANES, a JSON list kept in configuration, never in the repository: { name, endpoint, bucket, prefix, region?, accessKey, secretKey }. Each field is checked when read. A lane that fails a check shows as red, naming what is wrong but never its values. A duplicate name, or more than 32 lanes, is also red.
What changed:
src/snapshot/lanes.ts(new):
- readLanes reads every lane at once.
- The lister has only a page call, no put or delete, so the configured keys can be list-only.
- Each lane gets 10 s to answer.
- Errors pass through scrubReason (the lane's keys struck out) and are collapsed to one line.
- Bounded reads, never a full listing: one delimited listing per folder level (at most 3 pages of 1,000), walking only into the folder whose name sorts last. That assumes backup names carry their date, as the forge's snapshot names do. When a level is wider than three pages, a fresh object still proves the lane fresh; a stale one says to name a narrower prefix.
src/http/mcp/tools/backups.ts: the existing tool gains alanesfield (name,place,ok,newestwithkey/bytes/at/ageHours,says). No new tool;test/mcp-size.test.tspasses.src/tui/screens/resilience.ts: a "Backup lanes" section under the snapshots, one green or red line per lane with its reason.destination.tsgainsdestinationTarget, so the lanes follow the same precedence asresolveDestination.store.tsgainss3Where.docs/SNAPSHOT.md: a "Backup lanes" section with the configuration format.
Tests. test/backup-lanes.test.ts uses a fake store and covers:
- a fresh lane, a lane 27 h old, the 26 h edge, an empty lane, and a store that refuses or never answers;
- the bound (one page per level, last folder only);
- configuration checks;
- the S3 lister's request (
delimiter: "/",maxKeys: 1000) against a stubbedBun.S3Client; - the Resilience screen in a headless terminal.
test/mcp-backups.test.ts gets a lanes case, and every lanes check asserts that no key appears in the answer.
One existing assertion changed: the failing-listing test expected Bun.S3Client.list to be called once; it is now called twice, once for the snapshots and once for the destination's lane. That test was not wrong; the expectation is updated and still proves each call is tried only once.
Not done:
- The home line from #8753 is not built here; it should call
readLanesrather than add a second reader. - Nothing logs lane reads, so there is no log to scrub; the tests check the answers only.
Checks run here: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/hollow.ts are clean. scripts/fast.ts named 326 files (about 27 minutes), so I ran the 50 snapshot, backups and Resilience test files, mcp-size and the five cross files instead. Two cases in test/snapshot-destination.test.ts fail in this sandbox, and fail on main too, because the network allowlist blocks the connection they expect to be refused. The full suite runs on the push.
Try it: set FORGE_BACKUP_LANES as in docs/SNAPSHOT.md, then call MCP backups as an admin, or open Resilience from the terminal palette.
🤖 Generated with Claude Code
Every lane the platform declares in FORGE_BACKUP_LANES is now read from its own store and judged in the same backup verdict that #8753 landed on main. Home, vitals, MCP backups and the Resilience screen read that one verdict. The lane reader no longer has its own list: the forge's own primary and mirror lanes are read only once, by the bucket watch.
Merge with main. #8753 put a second reader on main: src/snapshot/bucket.ts, a timed watch over the forge's primary and mirror stores. Its lane record already noted that "a platform lane would add its own name", so the platform lanes now feed that watch:
src/snapshot/lanes.tsreads each declared lane into the watch's lane record, andwatchBuckettakes the declared lanes as a new last argument.src/snapshot/service.tspasses the reader, and a reader that throws does not stop the forge's own lanes from being kept.judgeinbucket.tsnow names each lane's newest object, so a lane reads like "backup/postgres newest pg/2026-11-02.sql.gz 3 h old, 3.0 MiB".- The
lanesfield I had added to MCPbackupsand my extra section on the Resilience screen are removed. Main'sverdict.lanesand main's verdict lines on the screen show every lane. - My
destinationTargetrefactor insrc/snapshot/destination.tsis dropped, since the forge's own lanes are nowbucket.ts's.
How a lane is read:
- Configuration: a JSON list of
{ name, endpoint, bucket, prefix, region?, accessKey, secretKey }, kept outside the repository. The forge only lists there, so the keys can be list-only. - Bounded listing: one delimited listing per folder level, at most three pages of 1,000, walking only into the folder whose name sorts last. That assumes backup names carry their date.
- Red, with the reason: when the store does not answer within 10 s, nothing is under the prefix, or the newest object is over 26 h old. When a folder is wider than three pages and the newest object found is stale, or none is found, the lane says to name a narrower prefix.
- Bad configuration: a lane that does not read whole is red, named, and never echoes its values.
- Credentials: errors pass through main's
scrubErrorand are cut to one line.
Tests. test/backup-lanes.test.ts uses a fake store. It covers a fresh lane, a lane 27 h old, an empty lane, a store that refuses and one that never answers, and the bound. It also covers configuration checks, a stubbed Bun.S3Client asked for delimiter: "/" and 1,000 keys, and one watch pass whose verdict and Resilience lines list primary beside a platform lane. Every lane check asserts that no key appears in the answer.
Checks run here: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/hollow.ts are clean. I ran 51 test files: those covering the bucket watch, the snapshot service, the lanes, Resilience, MCP backups and vitals, plus test/mcp-size.test.ts and the five cross files. 550 tests pass. The 2 failures are in test/snapshot-destination.test.ts; they fail on main too, because this sandbox's network allowlist blocks the connection they expect to be refused.
Try it: set FORGE_BACKUP_LANES as docs/SNAPSHOT.md ("Backup lanes") shows, wait for the watch's first pass, then call MCP backups as an admin and read verdict.lanes, or open Resilience from the terminal palette.
🤖 Generated with Claude Code
No high findings; the lane reader is bounded, list-only, validates config at the boundary and scrubs keys from errors. Two changes to make in one round:
- How-to location: the feature's how-to is in docs/SNAPSHOT.md only. Put the FORGE_BACKUP_LANES how-to (format, list-only keys, what turns a lane red, where to see it) in docs/features/backup-truth.md, which the bucket watch already owns, and leave SNAPSHOT.md a one-line pointer.
- Store-supplied text reaches the terminal unsanitised: the object key (newestName) is interpolated into the verdict line that the Resilience screen and MCP print. A key with escape or control bytes would reach a terminal. Strip control characters from keys in s3Lister (or when building the says text) and add a test with a key containing \u001b.
Smaller: add a test that MCP backups returns the platform lane in verdict.lanes (the tests cover backupVerdict and the TUI, not the tool), and a 26 h edge case (exactly 26 h green, just over red).
Every lane the platform declares in FORGE_BACKUP_LANES is now read from its own store, and the shared backup verdict names its newest object and age. That verdict is the one home, vitals, MCP backups (verdict.lanes) and the terminal's Resilience screen already read. This round makes all four of @sentinel's changes.
What a lane does:
src/snapshot/lanes.tsreads each declared lane and only ever lists, so its keys can be list-only. Each{ name, endpoint, bucket, prefix, region?, accessKey, secretKey }entry is checked when read.- The listing is bounded, never full: one delimited listing per folder level, at most three pages of 1,000, walking only into the folder whose name sorts last.
- A lane is red, with the reason, when its store does not answer within 10 s, nothing is under its prefix, or its newest object is over 26 h old. A lane that does not read whole is red and never echoes its values.
- The bucket watch reads the declared lanes beside
primaryandmirroron each pass; a declared reader that fails costs the forge's own lanes nothing.
Changes from the review:
- Control bytes from a store. A new
printable()insrc/snapshot/bucket.tsreplaces C0 and C1 control bytes and bidi overrides with?.judgeapplies it to the lane name, place, newest object name and error before they join a line.s3Listeralso cleans each key as it reads it, and a lane's error is cleaned too. - How-to location. The
FORGE_BACKUP_LANEShow-to now lives indocs/features/backup-truth.mdunder "Platform backup lanes": the format, list-only keys, the bounded read, what turns a lane red, and where to see it.docs/SNAPSHOT.mdkeeps a one-line pointer. The stale "platform lanes (#8805)" not-done line is removed. - New tests in
test/backup-lanes.test.ts:
- a key holding \u001b, read through the stubbed S3 client, comes back with the byte replaced;
- ESC, BEL, a C1 byte and a bidi override in a key or an error become ? in the verdict's text;
- a lane exactly 26 h old is green and one a minute past is red;
- MCP backups, called over HTTP after a watch pass, returns the platform lane in verdict.lanes with its newest object and no key in the answer.
Checks run here: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/hollow.ts are clean. 231 tests pass across 13 files: the backup-lanes, bucket-watch, probe, MCP backups, mcp-size, Resilience and vitals suites, plus the five cross files.
Try it: set FORGE_BACKUP_LANES as docs/features/backup-truth.md shows. After the watch's first pass, call MCP backups as an admin and read verdict.lanes, or open Resilience from the terminal palette.
🤖 Generated with Claude Code
Approved. The preview answers 200 at /. Lanes come from FORGE_BACKUP_LANES, so no lane list is in the repo. src/snapshot/lanes.ts only lists, in bounded pages. The bucket watch keeps the platform lanes beside primary and mirror, so MCP backups verdict.lanes and Resilience read one verdict. Store text is made printable and keys are scrubbed. Tests cover fresh, 27 h, 26 h edge, empty, refusing, hung and control bytes, and the how-to is in docs/features/backup-truth.md. Two things I would do next: (1) add an MCP and Resilience check with a real lane list on the preview, since the preview has no lanes configured and I could not see one live; (2) have the home line from #8753 read this verdict, and show the lane count in the dialog header so a missing FORGE_BACKUP_LANES is noticed.
Every backup lane now names its newest object (key, size, age), read from its own store. Lanes show in MCP
backupsand on the terminal's Resilience screen. A lane is red, with the reason, when its store does not answer, nothing is under its prefix, or its newest object is over 26 h old.Where lanes come from. The forge's destination is the
forge snapshotslane and its mirror isforge mirror; neither needs configuring. The platform declares the rest inFORGE_BACKUP_LANES, a JSON list kept in configuration, never in the repository:{ name, endpoint, bucket, prefix, region?, accessKey, secretKey }. Each field is checked when read. A lane that fails a check shows as red, naming what is wrong but never its values. A duplicate name, or more than 32 lanes, is also red.What changed:
src/snapshot/lanes.ts(new):-
readLanesreads every lane at once. - The lister has only apagecall, no put or delete, so the configured keys can be list-only. - Each lane gets 10 s to answer. - Errors pass throughscrubReason(the lane's keys struck out) and are collapsed to one line.src/http/mcp/tools/backups.ts: the existing tool gains alanesfield (name,place,ok,newestwithkey/bytes/at/ageHours,says). No new tool;test/mcp-size.test.tspasses.src/tui/screens/resilience.ts: a "Backup lanes" section under the snapshots, one green or red line per lane with its reason.destination.tsgainsdestinationTarget, so the lanes follow the same precedence asresolveDestination.store.tsgainss3Where.docs/SNAPSHOT.md: a "Backup lanes" section with the configuration format.Tests.
test/backup-lanes.test.tsuses a fake store and covers:delimiter: "/",maxKeys: 1000) against a stubbedBun.S3Client;test/mcp-backups.test.tsgets a lanes case, and every lanes check asserts that no key appears in the answer.One existing assertion changed: the failing-listing test expected
Bun.S3Client.listto be called once; it is now called twice, once for the snapshots and once for the destination's lane. That test was not wrong; the expectation is updated and still proves each call is tried only once.Not done:
readLanesrather than add a second reader.Checks run here:
bun scripts/gate.ts,bun x tsc --noEmitandbun scripts/hollow.tsare clean.scripts/fast.tsnamed 326 files (about 27 minutes), so I ran the 50 snapshot, backups and Resilience test files,mcp-sizeand the five cross files instead. Two cases intest/snapshot-destination.test.tsfail in this sandbox, and fail on main too, because the network allowlist blocks the connection they expect to be refused. The full suite runs on the push.Try it: set
FORGE_BACKUP_LANESas in docs/SNAPSHOT.md, then call MCPbackupsas an admin, or open Resilience from the terminal palette.🤖 Generated with Claude Code
Closes #8811