Skip to content

fix(#751,#752): retention counted deploys, and 30 of them was 3.5 days - #753

Merged
TortoiseWolfe merged 1 commit into
mainfrom
fix/751-retention-window-days
Aug 15, 2026
Merged

fix(#751,#752): retention counted deploys, and 30 of them was 3.5 days#753
TortoiseWolfe merged 1 commit into
mainfrom
fix/751-retention-window-days

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

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.txt carries a first-seen timestamp per
file; retention drops on elapsed days against RETAIN_DAYS (14). The generation counter
survives 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.mjs asked whether every promised
file 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:

mutation result
guard back to once-per-tab re-arm case fails in both harnesses
guard disabled entirely positive control reports the second-load result proves nothing
ledger span 2 days, past ramp window floor fails with the shortfall named
ledger span 20 days passes, window is at full width
ledger span 2 days, during ramp stays quiet — a narrow window is correct there

RETENTION_RETIMED_AT exists solely so that floor can be driven red before the ledger's
first fortnight elapses. A check nobody has seen go red is not yet a check.

Also fixed: pnpm test:scripts went from 301s to 2.3s — the new ASSET_AGES.txt fetch
revealed that check-retained-assets.test.js hung its own server for 300s on any
unexpected request.

Green: test:scripts 310/310, guard component tests 13/13, check-stale-html.mjs (all
five browser cases), lint, type-check, production build.

Not in this PR

#635 — Cloudflare in front of Pages, HTML no-cache and /_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

…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>
@TortoiseWolfe
TortoiseWolfe merged commit c528961 into main Aug 15, 2026
38 of 39 checks passed
@TortoiseWolfe
TortoiseWolfe deleted the fix/751-retention-window-days branch August 15, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants