fix(#751,#752): retention counted deploys, and 30 of them was 3.5 days - #753
Merged
Conversation
…was 3.5 days Production rendered unstyled for the eighth time on 2026-08-15. The origin was healthy — every route served CSS 200, all 13 retained stylesheets resolved, and the post-deploy detector was green. The visitor was holding a document the window no longer covered. RETAIN_GENERATIONS was raised 5 -> 30 in #650 on the reasoning that "30 covers a normal working week even at an unusually high merge rate". Measured against the deploy log for the six days after: 40 successful deploys, 19 in a single day. 30 generations was ~3.5 days, and the live ledger topped out at age 25. The comment above that constant already diagnosed the error one iteration earlier — the window protects how long a tab has been open, which is unrelated to deploy count. The fix acted on the diagnosis by changing the NUMBER and keeping the UNIT, so the same mismatch survived in a new parameterisation. So state it in the unit the risk is in. ASSET_AGES.txt now carries a first-seen timestamp per file, retention drops on elapsed days against RETAIN_DAYS (14), and the generation counter survives only as a diagnostic. A retained asset keeps its ORIGINAL date — restamping it would make the window never expire anything while looking healthy, so that has its own test. RETAIN_MAX_FILES (800) is a runaway backstop, and it says so loudly when it engages, because a count deciding coverage means the day window no longer is. Nothing could have caught the shortfall. check-retained-assets.mjs asks whether every promised file is served; all 13 were. It never asked whether the promise was wide enough. It does now, and that assertion fails rather than warns — this file's own header records how a warning inside a green check went unnoticed. It is dormant during the ledger's first fortnight, because a freshly-retimed ledger legitimately spans zero days, so RETENTION_RETIMED_AT exists purely so the floor can be exercised before then: a check nobody has seen go red is not yet a check. StylesheetGuard is the recovery that depends on no number, and it was disarming itself for the life of a tab after one recovery — stranding exactly the visitors it exists for, since holding a document for days is what expires its assets. It already stored a timestamp; only the read side threw it away. Now it re-arms after an hour, which still stops a genuine loop (those retry in seconds). Verified by mutation, not by assertion count. Reverting the guard to once-per-tab fails the new re-arm case in both harnesses; disabling it entirely trips the positive control, which reports that the second-load result proves nothing rather than quietly passing. The window floor was driven red at a 2-day ledger and green at 20, with the ramp case proving it stays quiet when a narrow window is correct. The StylesheetGuard unit test asserted `sessionStorage.getItem(...) return` by regex — a shape match that passed for the bug being fixed. The throttle is sessionStorage plus arithmetic, which jsdom runs faithfully, so it is executed now instead of pattern-matched. None of this is the fix. #635 is: a CDN serving HTML no-cache, which GitHub Pages cannot be configured to do. Closes #751 Closes #752 Refs #635 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Production rendered unstyled for the eighth time on 2026-08-15. The origin was
healthy throughout — every route served CSS 200, all 13 retained stylesheets resolved,
and the post-deploy detector was green. The visitor was holding a document the retention
window no longer covered.
Findings and rationale live in the issue bodies: #751 (the window counted deploys)
and #752 (the self-heal disarmed itself). #635 stays open — it is the actual fix.
What changed
The window is a duration now.
ASSET_AGES.txtcarries a first-seen timestamp perfile; retention drops on elapsed days against
RETAIN_DAYS(14). The generation countersurvives as a diagnostic only. A retained asset keeps its original date — restamping
would make the window never expire anything while looking healthy, so that has its own
test.
RETAIN_MAX_FILES(800) backstops a runaway and announces itself when it engages,because a file count deciding coverage means the day window no longer is.
The width is now asserted.
check-retained-assets.mjsasked whether every promisedfile is served; all 13 were, on the night this broke. It never asked whether the promise
was wide enough. It does now, and it fails rather than warns.
The self-heal re-arms after an hour instead of disarming for the life of the tab —
which stranded exactly the visitors it exists for, since holding a document for days is
what expires its assets. Loop protection is intact: a genuine loop retries in seconds.
Verification
Mutation-driven, not assertion-count-driven:
window is at full widthRETENTION_RETIMED_ATexists solely so that floor can be driven red before the ledger'sfirst fortnight elapses. A check nobody has seen go red is not yet a check.
Also fixed:
pnpm test:scriptswent from 301s to 2.3s — the newASSET_AGES.txtfetchrevealed that
check-retained-assets.test.jshung its own server for 300s on anyunexpected request.
Green:
test:scripts310/310, guard component tests 13/13,check-stale-html.mjs(allfive browser cases), lint, type-check, production build.
Not in this PR
#635 — Cloudflare in front of Pages, HTML
no-cacheand/_next/static/*immutable.That removes the class rather than widening it, and its runbook (including copying MX/TXT
before the nameserver change) is already written up in the issue. Owner's go-ahead.
Closes #751
Closes #752
Refs #635