Skip to content

feat(web-pace): add an unwired pacing reducer prerequisite - #218

Draft
jonybur wants to merge 2 commits into
mainfrom
feat/adaptive-pace-core
Draft

feat(web-pace): add an unwired pacing reducer prerequisite#218
jonybur wants to merge 2 commits into
mainfrom
feat/adaptive-pace-core

Conversation

@jonybur

@jonybur jonybur commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

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

  • Per-origin rule growth from a trusted block signal, with bounded Retry-After input.
  • Lazy decay and explicit retirement.
  • A versioned persisted shape validator. Invalid state cannot authorize an action.
  • Catch-up waits with bounded jitter and conservative clock handling.
  • A one-action probe state machine. A lower interval is adopted only after a reserved action, a trusted clean result, the probe window, and a settle period.
  • A terminal ceiling decision that never returns an executable wait.
  • No model-facing rule mutation or clear operation.

What is still required

  • Exact canonical origin keys and one serialized service-worker lane shared across sessions.
  • Trusted persistent storage with fail-closed hydration for writes.
  • Fixed Retry-After and structural challenge classifiers. Arbitrary page text must not become a signal.
  • Pre-action and post-action hooks across every browser write path, including relays.
  • Abort, Stop, deadline, origin, tab-liveness, and policy rechecks after every wait.
  • Trusted terminal handoff results that prevent retry or delegation around the policy.
  • A human-only Paced sites settings list, individual forget controls, and audit events.
  • Accessible live wait status with Stop.
  • Integration, browser, E2E, and visual coverage for the completed shell.

Validation

  • bun test tests/peerd-runtime/web-pace-reducer.test.ts
  • bun test ./tests
  • bun run typecheck
  • bun run check:tscheck
  • bun run lint
  • git diff --check

Keep this PR in draft until the service-worker, cancellation, persistence, relay, handoff, and user-control shell is designed and implemented.

Copy link
Copy Markdown
Owner

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:

  1. Rebase: packaging/check-tscheck.ts — main's COVERED_FLOOR has moved 522 → 530 since this branch; bump to 531.
  2. Repoint the spec ref: web-pace/reducer.js's header cites docs/store/ADAPTIVE-PACING.md (PR docs(store): spec adaptive per-origin action pacing (ANTI-BOT Option 0) #213) — that PR is closed per the no-design-docs policy; the spec + open decisions (A–F) + this branch's deliberate deltas now live in Adaptive per-origin action pacing (ANTI-BOT-POSTURE Option 0 resolution) #234.

Generated by Claude Code

Copy link
Copy Markdown
Owner

Held back from the merge sweep, but only on mechanics — no defect was found in the reducer itself. Merged onto main in a throwaway worktree: bun test tests/peerd-runtime/web-pace-reducer.test.ts 21 pass / 0 fail, bun run typecheck (strict, the file carries // @ts-check) clean. Conventions all respected — vanilla JS, // why: rationale throughout, imports only clamp from /shared/util.js, no generated-file or dweb-boundary violations.

What stops it landing today:

  • It conflicts with main and can't be API-merged. packaging/check-tscheck.ts bumps the floor 521 → 522 against a base where it had already moved to 530 — and The security arc, integrated — one branch to test #255 has since taken it to 544, so the branch is now 30+ commits behind rather than 22.
  • The floor number needs re-deriving after the rebase, not just conflict-resolution. Against the pre-The security arc, integrated — one branch to test #255 base it resolved to 531, not 522; against today's main it'll be higher again. Run bun packaging/check-tscheck.ts after rebasing and take the reported count. (This is a ratchet-hygiene issue, not a red gate — the check only warns when coverage sits above the floor.)
  • CI last ran 2026-07-16 on a base that no longer exists in any meaningful sense. Needs a fresh run before it's judgeable.

Two content notes for whenever you rebase: the module and test headers both cite docs/store/ADAPTIVE-PACING.md (PR #213), which doesn't exist on main (docs/store/ has ANTI-BOT-POSTURE.md but no adaptive-pacing doc) — either #213 lands first or the reference should soften. And isRetired(newRule(...)) is true out of the box, since minIntervalMs 0 < retireFloorMs 250, so a never-blocked rule reads as "retirable". Harmless if callers only mint lazily, but worth an explicit comment when the storage shell arrives.

Landing this adds an unreferenced file — git grep web-pace on main returns nothing, so only the tests exercise it. That's consistent with the stated pure-core scope; flagging it just so the sequencing with the enforcement hooks is deliberate.


Generated by Claude Code

@jonybur
jonybur force-pushed the feat/adaptive-pace-core branch from d9481d5 to fa78523 Compare July 31, 2026 15:45
@jonybur

jonybur commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main. Only conflict was the // @ts-check coverage floor (main had moved it to 544); resolved to main's value and bumped to 545 for this branch's new checked file. Gates green after the rebase: 3445 bun, 631 in-browser, typecheck, lint, boundary, imports, tscheck.

jonybur and others added 2 commits August 6, 2026 16:07
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>
@NotASithLord
NotASithLord force-pushed the feat/adaptive-pace-core branch from fa78523 to dfd30f3 Compare August 6, 2026 14:47
@NotASithLord NotASithLord changed the title feat(web-pace): the pure policy core of adaptive per-origin pacing (#213) feat(web-pace): add an unwired pacing reducer prerequisite Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

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.

@NotASithLord

NotASithLord commented Aug 8, 2026

Copy link
Copy Markdown
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants