feat(web-pace): add an unwired pacing reducer prerequisite - #218
Conversation
|
Triage outcome: keeping this open as the active vehicle for adaptive pacing — the pure core is real, tested (21 cases incl. the raise-only safety property), and stands alone per the functional-core convention; it stays unmerged until the storage/hooks/tool wiring lands with it. Two things for the next push:
Generated by Claude Code |
|
Held back from the merge sweep, but only on mechanics — no defect was found in the reducer itself. Merged onto What stops it landing today:
Two content notes for whenever you rebase: the module and test headers both cite Landing this adds an unreferenced file — Generated by Claude Code |
d9481d5 to
fa78523
Compare
|
Rebased onto current |
Implements the reducers specified in docs/store/ADAPTIVE-PACING.md (#213) — the "learned, not hardcoded; code, not prompt" direction. Pure core only: no storage, no hooks, no tool. The clock and RNG are injected, so every transition is pinned exactly under Bun. observe -> learn -> enforce -> decay/probe: - nextRuleOnBlock: sizes the interval from the cadence that ACTUALLY got blocked (x slowdownMult), compounds on repeats, honors a larger Retry-After, clamps at maxPaceMs. A seed floor (a K the spec's list did not enumerate) makes a block with no cadence sample still learn a rule — otherwise max() over unknowns is 0 and a real block teaches nothing. - decay: lazy + MULTI-period (rules are recomputed on access, not on a timer — spec open question C), so five quiet periods decay five steps. Never mid-probe, never on a backwards clock. isRetired() tells the caller to drop the record: no permanent tax. - waitForAction: a catch-up, not a fixed tax. First action free; natural latency counts; +/- jitterFrac de-regularizes the tempo; the trial interval is what a probe enforces. - startProbe/resolveProbe: the reversible descent. Clean adopts the lower value (possibly retiring the rule); blocked snaps back, re-escalates from the trial cadence, and resets the quiet timer. - needsHandOff: at the ceiling the caller escalates to the posture ladder rather than silently napping a turn. DESCENT IS CONSTRAINED (a deliberate change from the spec's tool sketch). The spec's pace_rule offers set/clear clamped to [0, maxPaceMs] and re-checked SW-side. But an SW re-check validates a call's ARGS, never its INTENT — it cannot tell "the actor judged this rule stale" from "a page talked the actor into clearing it", and the web actor is precisely the heap that ingests untrusted page bytes. The asymmetry matters: raising is self-limiting (peerd is slow on one origin), while lowering is what an injected page wants — retire the rule, peerd hammers, and the user's own logged-in account eats the ban. So there is no clearRule() and applyAgentSet() is RAISE-ONLY; the only ways down are automatic decay and a probe that reverts on a block. That costs nothing: probe already IS the owner's stated use case ("test whether it's still needed"). Tests pin the arithmetic and the safety property, including that the export surface offers no clear/reset hatch. Signed-off-by: Jonathan Bursztyn <jobur93@gmail.com>
fa78523 to
dfd30f3
Compare
Visual regression✅ No visual drift. Every state renders identically to the committed baselines. 📸 Browse the visual gallery — 44 baseline screens (light + dark) as of this commit. |
|
Adversarial review found one core blocker before this reducer can be integrated. Retry-After is treated as an interval from the prior action, not as a deadline from the block response. Example: an action at t=0, a 429 observed at t=5s, and Retry-After: 9s can yield only 4s before the next action with neutral jitter, or 1.3s with the current negative jitter. The server requested 9s after the response. The reducer needs a trusted not-before timestamp derived from observation time. Jitter must never move execution earlier than that server minimum. The downward jitter rationale should also be removed because pacing is for respecting site limits, not disguising automation. The PR remains draft for the reasons already in its description: it is unwired, conflicts with current main, and does not yet provide the persistence, cancellation, enforcement, user controls, or model handoff required by #234. Focused reducer tests pass, but they do not cover this timing case. |
Draft prerequisite for #234.
This PR adds the deterministic reducer and its tests only. It is not wired into the extension. No pacing rule is observed or stored, no page action is delayed, and no handoff or user surface exists yet.
What is here
What is still required
Validation
bun test tests/peerd-runtime/web-pace-reducer.test.tsbun test ./testsbun run typecheckbun run check:tscheckbun run lintgit diff --checkKeep this PR in draft until the service-worker, cancellation, persistence, relay, handoff, and user-control shell is designed and implemented.