Skip to content

feat(daemon): per-site navigation pacing and security-block circuit breaker - #2464

Open
yisiliu wants to merge 1 commit into
jackwener:mainfrom
yisiliu:feat/site-pacing
Open

feat(daemon): per-site navigation pacing and security-block circuit breaker#2464
yisiliu wants to merge 1 commit into
jackwener:mainfrom
yisiliu:feat/site-pacing

Conversation

@yisiliu

@yisiliu yisiliu commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Why

xiaohongshu's own pitfalls doc (sitemaps/xiaohongshu/pitfalls.md) documents that risk control triggers on velocity and asks callers to keep 1–2s between requests and to back off 60s after a 安全限制 — but nothing enforces any of it. Adapters jitter their own settle waits, yet navigations from separate CLI invocations land back-to-back: a batch caller turns one hiccup into a burst of rapid page loads, which is the documented escalation path toward account restrictions (#842, #677). The soft-block retry helper explicitly notes cross-invocation throttling as a follow-up (risk-control.js: "does NOT cap request velocity across separate CLI invocations (that needs session-level throttling, tracked as a follow-up)") — this PR is that follow-up.

Design

Same architecture argument as session-lease.ts: the daemon is the one local process that sees every CLI client, so it arbitrates centrally, with the decision logic in a pure, injectable-clock module (src/site-pacing.ts) and only thin wiring in the daemon handler.

  1. Navigation slots — adapter navigate dispatches for a paced (contextId, site) are spaced by a jittered per-site interval (xiaohongshu 1.5–3s, weibo 1–2s; all other sites untouched). Slot assignment is max(now, lastSlot + interval), so concurrent CLI processes serialize into consecutive slots without locks, and a navigation arriving after the interval passes with zero delay. Only navigate is paced — evaluates against a warm tab load no pages, and pacing them would slow everything for nothing.

  2. Circuit breakerexecution.ts reports command outcomes best-effort on settle: SECURITY_BLOCK failures and successes only (unrelated errors say nothing about the site's risk state, unknown-outcome endings report nothing). Two blocks inside a 10-minute window open a randomized 5–10 min cooldown; while open, paced navigations fail fast with a machine-readable security_cooldown (HTTP 429) carrying retryAfterMs, so schedulers can re-queue instead of being silently held for minutes — and the hot risk state is never hammered. Any success closes the loop.

  3. Observability — the daemon's own stdio is discarded (stdio: 'ignore'), so pacing events also land in the /logs ring buffer: [pacing] spacing xiaohongshu navigate by 1881ms / [pacing] xiaohongshu navigate refused — security cooldown, retry in 312s.

  4. Kill switchOPENCLI_PACING=off at daemon startup disables the layer.

Verification

  • TDD throughout: 24 new tests written first. site-pacing.test.ts (20) covers slot serialization, jitter bounds, zero-delay after idle, contextId partitioning, breaker open/close/reset/window-pruning, the classify predicate, the 429 failure shape, and locks the shipped defaults to the pitfall-doc bounds. execution.test.ts (4) covers the report contract: ok on success, security_block on SECURITY_BLOCK, nothing on unrelated failures or non-browser commands.
  • Layer coverage stated explicitly: the daemon handler wiring is thin and untested at handler level (same status as the session-lease wiring); every decision it delegates to is unit-tested.
  • Live, against the real daemon + xiaohongshu: three parallel note invocations completed in a clean staircase (4.8s / 7.3s / 10.3s) with /logs showing spacing xiaohongshu navigate by 1881ms and 3647ms — consecutive slots, jitter in bounds. Sequential invocations correctly pass undelayed (their natural spacing exceeds the interval). The breaker path is deliberately not live-tested — intentionally triggering real security blocks escalates account risk; it is unit-covered.

Part of the persistent-session/risk-control series: weibo #2442 → xiaohongshu #2460/#2461 → this.

🤖 Generated with Claude Code


Part of the xiaohongshu risk-control/persistence arc — umbrella issue with full motivation and cross-PR context: #2470

…reaker

Sites with velocity-based risk control (xiaohongshu, weibo) soft-block
accounts that navigate faster than a human. Adapters already jitter
their own settle waits, but nothing spaced navigations across separate
CLI invocations - a batch caller fires them back-to-back, which is
exactly the pattern the xiaohongshu pitfalls doc warns escalates toward
account restrictions.

The daemon is the one local process that sees every CLI client (same
argument as session-lease.ts), so it now enforces two rules centrally,
via a pure injectable-clock module (src/site-pacing.ts):

- Adapter `navigate` dispatches for a paced (contextId, site) are spaced
  by a jittered per-site interval (xiaohongshu 1.5-3s, weibo 1-2s).
  Concurrent CLI processes serialize into consecutive slots without
  locks; navigations arriving after the interval pass with zero delay.
  Only `navigate` is paced - evaluates against a warm tab load no pages.

- Two SECURITY_BLOCK outcomes within 10 minutes open a randomized
  5-10 min cooldown during which paced navigations fail fast with a
  machine-readable `security_cooldown` error carrying retryAfterMs,
  instead of hammering a hot risk state. Any successful command resets
  the counter. execution.ts reports outcomes best-effort on command
  settle (SECURITY_BLOCK errors and successes only - unrelated failures
  say nothing about the site's risk state).

Pacing events surface in the daemon's /logs ring buffer (its own stdio
is discarded). OPENCLI_PACING=off at daemon startup disables the layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant