feat(pacing): honor the pause a site asks for, end to end - #437
Conversation
Issue #234, landed as one enforcement slice rather than an unwired policy core. PR #218 was closed for being the latter, and its closing review named the defect this must not repeat: Retry-After was stored as an interval measured from the PRIOR action, and negative jitter could then move an action earlier still than the instant the server named. Both halves are fixed and pinned by tests. The recorded deadline is ABSOLUTE and anchored to the response that stated it, so it survives a service-worker restart and a later smaller answer cannot shorten it. The only additive term anywhere is a fixed positive-only clock-skew guard; nothing can release an action inside a stated window. There is no tempo de-regularization: the posture is compliance, not disguise. Two brakes per origin, both minted only by the egress choke point holding a real Response: - notBeforeMs, an absolute deadline, gates every request, read or write. - minIntervalMs, a learned floor, gates browser write actions. Enforcement has two points. The dispatcher meters browser actions on the origin the action is about to touch - the destination for a navigation, the live tab origin for an in-page action - and waits inside the per-origin FIFO lane, which is what stops concurrent sessions passing the limiter together. web-fetch.js meters network requests, and holds them for a much shorter budget because every caller runs it inside its own fetch timeout. Past the inline ceiling peerd stops instead of napping: a fixed refusal that ends the turn. Because the deadline is durable and keyed by origin rather than session, a retry or a fresh actor meets the same refusal. Nothing reachable from a tool can move a rule. The dispatcher gets two read-shaped closures; the policy core exports no setter. Descent is time decay or a person on Settings -> Paced sites. Unreadable state refuses browser writes rather than reading as "no limits". The side panel explains a wait instead of looking hung, naming the site and counting down locally off the deadline, with the way out in the same row. Verified: 6482 bun, 976 in-browser, 42 e2e states / 316 checks (including a new one that drives a real seeded rule through the real dispatcher and proves Stop cancels the pending action), 15/15 red-team scenarios with 11 new probes behind a new INV-21, lint, typecheck, boundary, imports, pages, copy, invariants. Deliberately not in this slice: the one-action descent probe from #234. It is the only mechanism that acts FASTER than the learned rule, and decay plus the human forget control already cover descent. It should land once the enforcement half has field evidence. Signed-off-by: Jonathan Bursztyn <jobur93@gmail.com>
Visual regressionoptions-paced-sites -
|
|
The visual lane reports 18 of 70 screens moved. All 18 are accounted for, and all are intended:
No side-panel screen moved. The wait bar renders only during an actual paced wait, which the new |
An observation arriving while the durable record is unreadable would have persisted a partial state built from whatever this session happened to see - overwriting the evidence while browser writes still failed closed, since loadFailed does not clear itself. The record now survives untouched, and the human "forget all" remains the one sanctioned way back to a readable one. Also commits the Gecko badge count the CI job measured (970/970); the pinned Firefox is not installed locally. Signed-off-by: Jonathan Bursztyn <jobur93@gmail.com>
Signed-off-by: Jonathan Bursztyn <jobur93@gmail.com>
That fix lives in #439, not here. Claiming it in this PR's entry would credit this change with something it does not contain. Signed-off-by: Jonathan Bursztyn <jobur93@gmail.com>
`strikes` was written on every block, validated on every read, and cleared by a whole transition of its own - and no decision ever consulted it. Escalation already compounds from the rule's own current interval, and decay is what walks that back down. Removing it also removes the write it caused on every successful request to a paced origin, since recording "we were fine once" was that path's only effect. Persisted state that nothing reads is state that drifts. Signed-off-by: Jonathan Bursztyn <jobur93@gmail.com>



















Closes #234.
What broke
Nothing shipped learned or enforced a site's stated rate limit. PR #218 was closed for being an unwired policy core, and its closing review named the defect to not repeat:
Retry-Afterwas stored as an interval measured from the prior action, and negative jitter could move an action earlier still than the instant the server named.What changed
Per-origin pacing, as one enforcement slice.
notBeforeMsis anchored to the response that stated it, survives a service-worker restart, and a later smaller answer cannot shorten it. The only additive term is a fixed positive-only clock-skew guard, so nothing can release an action inside a stated window. No tempo de-regularization: the posture is compliance, not disguise.web-fetch.jsmeters network requests on a much shorter budget, because every caller runs it inside its own fetch timeout.Honest limits, stated
Only requests peerd makes through its own network path expose a status and a
Retry-After. A page navigation does not, on any package. Requests a page makes for itself, including anything an action causes, are the page's own and are never metered.Not in this slice
The one-action descent probe from #234. It is the only mechanism that acts faster than the learned rule; decay plus the human forget control already cover descent. It should land once the enforcement half has field evidence.
Verified
6482 bun · 976 in-browser · 42 e2e states / 316 checks · 15/15 red-team scenarios (11 new probes behind a new INV-21) · lint, typecheck, boundary, imports, pages, copy, invariants, preflight.
The new e2e state drives a real seeded rule through the real dispatcher and proves Stop cancels the pending action rather than letting it fire late.
badges/inbrowser-gecko.jsonis not refreshed here: the pinned Firefox is not installed locally. Its count will come from the Gecko job.