> For the complete documentation index, see [llms.txt](https://fleet.hackmap.win/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://fleet.hackmap.win/meta/changelog.md).

# Changelog

## 1.2.6 — 2026-07-09

* **Fleet is now fully-setup — no optional extras.** The `[browser]`, `[cloak]`, `[cloudflare]`, `[otel]`, and `[all]` extras are gone; their deps (cloakbrowser, mitmproxy, curl\_cffi, pyvirtualdisplay, uaforger, and the OpenTelemetry SDK) are mandatory in the base `dependencies`. A plain `pip install fleet-framework` gives you the master, worker, browser pool, anti-bot helpers, and tracing. A missing dependency now surfaces as a plain `ImportError` (an invalid install) rather than being silently degraded.
* **All imports live at module top.** Every in-function/lazy import was hoisted to the top of its file, and the `try/except ImportError` degradation guards (browser pool, curl\_cffi, opentelemetry) were removed. The one circular import this exposed — the concrete backends importing `RawItem`/`task_id_from_body` from `fleet.core.backend` — was resolved by extracting those into a leaf module `fleet.core.queue_types`.
* **Removed the external `serp_plus` router hook.** The master no longer conditionally mounts `serp_plus.api` (an out-of-tree plugin); fleet core carries no external-plugin import glue.

## 1.2.5 — 2026-07-09

Audit-driven bug fixes across the core state machine, worker/master lifecycle, browser pool, storefront billing, and Cloudflare solver, followed by a slop-cleanup pass.

* **Queue delivery is no longer duplicate-prone.** `queue_reserve` and the scheduled-retry drain were read-then-write races on Redis: two reservers could pop the same due retry and deliver it twice (double side effects). Reserve is now an atomic `LPOP`+register Lua call, and the scheduled drain is a single atomic range+remove+requeue — a task is delivered once. A crash between pop and register can no longer lose the task, and a poison (non-JSON / id-less) body is delivered once and dropped instead of looping forever through the DLQ. Behavior is now consistent across the Redis, SQLite, and in-memory backends.
* **Reconcile no longer drops config on scale.** Changing a restart-class field (e.g. `proxy_url`) together with the slot count previously scaled and silently dropped the field change; the diff now returns `[scale_up, restart_pages]` so both the new slots and the recycled ones run the new config. Scale actions carry the desired config; slot ids stay unique across heal / scale-down→scale-up.
* **Worker stays connected through handshake rejections.** The WS reconnect loop caught only transport errors, so an HTTP-status handshake rejection (e.g. 403 during token rotation) killed the client permanently; it now retries all `WebSocketException`s with backoff. In-flight outbound frames are re-buffered instead of dropped on a failed send. `SlotRunner.aclose` (previously dead-code, misindented) now actually closes the backend.
* **Master task bookkeeping.** A failed `queue_push` now releases its idempotency key instead of blocking re-submits for 24h; `dlq_replay` re-enqueues before removing so a mid-batch failure can't drop the remaining tasks.
* **Browser lifecycle leaks closed.** Partial `start()` / `acquire()` failures no longer strand the SmartRouter `mitmdump` subprocess, Playwright driver, or Chromium; the pool disposes in-flight spawns on `stop()` and no longer hangs forever when every worker fails to start. Percent-encoded proxy credentials are now URL-decoded. Pool-item claims are only released by the still-owning holder, so an expired-then-reclaimed item isn't stolen.
* **Storefront billing correctness.** Concurrent calls can no longer collectively exceed a credit quota (atomic check-and-deduct via `try_charge`); a mid-month plan change updates the ledger immediately; refunds spanning a month boundary anchor to the original charge's period; `ApiKey.rate_limit_rpm` is now enforced (429 on excess); the product catalog persists across restarts; out-of-range product creates return 422 instead of 500.
* **Cloudflare solver actually runs.** `CloudflareSolver.solve` was sync and returned an un-awaited coroutine; it's now `async` (and the `Solver` protocol matches). A transient empty page title no longer aborts the challenge-retry loop as a false success.

## 1.2.4 — 2026-05-22

* **SERP+ is now a CTR booster, not just a rank tracker.** The plugin's contract was misaligned: it would type the query, walk the SERP, find the target, and emit a `position` — but never actually click the result. Real value of SERP+ is the dwell-after-click signal Google's ranker reads. The flow now closes that loop: `ClickResult(rank=N)` clicks the target's link via cloak.human, then `ReadPage(target_dwell_ms, max_scrolls)` dwells on the target site with realistic scroll, optionally followed by `internal_clicks` same-origin clicks for deeper engagement.
* **`RankTrackTask` new fields**: `click_target=True`, `target_dwell_ms=30_000`, `target_max_scrolls=8`, `internal_clicks=0`, `bounce_back_to_serp=False`.
* **`RankResult` new fields**: `clicked`, `landed_url`, `target_dwell_actual_ms`, `target_host_paid_bytes`, `target_host_direct_bytes`, `target_host_requests`.
* **Target-stays-direct invariant + receipt.** `target_host_paid_bytes` is computed after every run by replaying the SmartRouter's per-request log and counting wire bytes whose host matches `target_domain`. The invariant says it MUST be 0 — non-zero means a `paid` rule is misconfigured and target-site bytes are burning residential quota. The plugin logs a loud `target-stays-direct invariant VIOLATED` and surfaces the count on the emitted result. New tests at `tests/serp/test_target_host_receipt.py` cover the receipt computation, span-window filtering, and the invariant-violation detection path.

## 1.2.3 — 2026-05-22

* **SERP+: human-typed search.** The plugin no longer navigates to `https://www.google.com/search?q=…` directly — that was a major realism gap (no typing, no autocomplete XHRs, no homepage warm-up). New flow: `OpenSite(engine.home_url)` → `Search(query, rhythm=…)` (letter-by-letter via cloak.human with mistypes and think-pauses) → `ReadPage`. Configurable via `typing_rhythm` and `per_page_dwell_ms`.
* **SERP+: pagination.** When the target isn't in the first page of organic results, the plugin now clicks the engine's next-page link (selectors per-engine, fallback to `&start=N` / `&first=N`) and continues up to `ceil(depth / results_per_page)` pages. `RankResult.position` is global (page-offset adjusted); `RankResult.page` is the page where the target was found; `RankResult.pages_walked` reports how many pages were scanned.
* **Per-action byte accounting.** Every action span (`open_home`, `search`, `page_N_read`, `paginate_to_N`) is timestamped; after the run, requests from the SmartRouter's per-request log are bucketed into those spans and emitted as `RankResult.action_bytes: list[ActionBytes]` plus `total_paid_bytes` / `total_direct_bytes`. Lets you answer "how much residential proxy did this single rank-track cost?" per action, not per request.
* **Property test: target host stays direct.** `tests/serp/test_router_preset_target_direct.py` walks the `serp_plus` preset against a parametrized list of innocent hosts (nike.com, amazon.com, github.com, …) and asserts every one resolves to the `direct` action. Locks the invariant: clicking through to a target site never spends residential proxy bytes.

## 1.2.2 — 2026-05-21

* **`SmartRouter` now forwards brotli shared-dictionary headers.** The curl\_cffi outbound used by paid flows previously stripped every request header that wasn't in its allowlist, so Chrome's `Available-Dictionary: :<sha256>:` (and the matching `Dictionary-Id` / `Use-As-Dictionary`) never reached the upstream — meaning Google could not compress the SERP against the cached homepage shared dictionary. The allowlist now includes those three plus `Accept-Encoding` so Chrome's dict-aware negotiation completes end-to-end. On the response side, `Content-Encoding: dcb` / `dcz` is now preserved (previously stripped along with `br` because curl\_cffi auto-decompresses standard encodings) so the browser receives the dict-compressed body and decodes locally. Expected warm-cache saving on `/search`: 30–50 %.
* **fleet\_serp\_plus public exports**: `ConsistencyChecker` and `ConsistencyVerdict` added to the top-level namespace.

## 1.2.1 — 2026-05-21

* **Refactored `FingerprintFactory` onto the new uaforger 0.1.5 API.** Switched from manual candidate-walking to `UserAgentGenerator.generate(families="chrome", device_types="desktop", min_chromium_version=N)`. \~200 lines collapsed to \~50; behaviour preserved; uaforger now also ships Chrome OS templates, so the default OS distribution is back to the natural 72/18/8/2 (Win/macOS/Linux/CrOS). Browser version still pinned to cloak's bundled major (146) via rejection-sampling. PyPI package name is `uaforger` (the import remains `import uaforge`); pinned in `[browser]` extras.

## 1.2.0 — 2026-05-21

* **`fleet_browser.human.actions` — Layer 2 vocabulary.** Page-aware browser actions: `OpenSite`, `Search`, `ClickResult`, `ReadPage`, `ScrollAndDwell`, `BackNavigate`, `ClickLink`, `LeavePage`. Each is a dataclass with `async execute(page, hctx) -> ActionResult` + `estimated_duration_ms()`. Plus `SearchEngine` protocol and a Google engine adapter (`GoogleEngine` in `fleet_browser.human.engines`) that knows the SERP's selectors and detects the shell-vs-real two-stage flow.
* **`fleet_browser.human.journey` — Layer 3 planner.** `Journey(target, personality, budget_seconds, start_strategy, detour_pool, seed, dwell_speed_multiplier)` — stochastic state machine that pre-plans an action sequence: 0 to N distractor detours (count from personality), then the target. Budget-low fallback forces a direct nav so the target is always reached. Four personalities (`FOCUSED`, `CASUAL`, `DISTRACTED`, `RESEARCHER`) tune detour count, typing rhythm, dwell distributions, and start-strategy probabilities. Default distractor pool (Wikipedia, HN, BBC, Reuters, etc.); register custom pools via `register_pool(name, distractors)`. `dwell_speed_multiplier` scales all dwells so tests can run instantly without mocking time.

## 1.1.0 — 2026-05-21

* **`fleet_browser.human` — human-shaped motion layer.** Re-exports `cloakbrowser.human` primitives (bezier mouse, realistic typing, idle drift, smooth wheel-scroll) under fleet's namespace with explicit `human.move/click/type_text/idle/scroll_to_element` helpers. Plus `timing.lognormal_ms / gamma_ms / uniform_ms` distribution samplers for plugin-side timing.
* **`BrowserNeeds.human_motion: bool = True`.** Activates the human layer by default. Plugins drive `page.click` / `page.type` / `page.mouse.*` as usual and get bezier curves + lognormal delays for free. Opt out by setting `False` for legacy plugins with their own timing. See [Human-shaped motion](/guides/browser-automations.md#human-shaped-motion-fleet_browserhuman).
* **`FingerprintFactory` now uses uaforge's real market-share weights.** Default OS distribution is 72/18/8 Windows/macOS/Linux (Chrome desktop\_weights from uaforge data; Chrome OS dropped — no template). OS sub-versions sampled by their `probability` field (Win 10 50 % / Win 11 50 %; macOS 13.5.1, 14.2.0, 15.0.0; etc.). Chrome major version pinned to cloak's bundled binary (Chromium 146) to avoid UA-vs-engine mismatch — override via `pinned_chromium_major=None` if you accept the risk.
* **Motion validator test rig at `tests/browser/human_validator/`.** Self-contained HTML page with six checks (mouse trajectory curvature, click hover-before-down + non-instant duration, typing rhythm stdev + think-pauses, smooth wheel-scroll, idle drift, fingerprint introspection) + a Python harness that exercises cloak.human and prints a pass/fail report.
* **Wiring docs clarification.** Made the producer-stream identity rule explicit in `docs/guides/inter-automation.md`: `ctx.emit(payload)` writes to a stream **named after the automation's `@register("…")` string**; consumers reuse the same name in their `Stream[Payload]("…")` reference.

## 1.0.1 — 2026-05-21

* **`SmartRouter` per-request log + event callback.** The mitmproxy addon writes one JSONL line per completed flow next to its stats file (`{t, host, path, method, action, status, wire, [cache], [truncated]}`). `RouterStats.request_log_path` surfaces the path; `router.requests(limit=N)` reads the tail; `router.set_event_callback(cb)` streams batches to a callback via a background drain thread, so a master / dashboard can subscribe without blocking the addon. See [Per-request telemetry](/guides/browser-automations.md#per-request-telemetry).
* **`fleet-provider-flameproxies`.** Third built-in proxy provider, sharing the same `register_provider` entry-point as Evomi and Dataimpulse. Auto-registered; takes a Flameproxies account + package + country + sticky-lifetime and assembles `<account>-package-<pkg>[-country-<cc>]-session-<sid>-time-<min>` as the proxy username. See [Flameproxies](/guides/proxy-providers.md#flameproxies).

## 1.0.0 — 2026-05-19

First stable release. The public API in `fleet.core`, `fleet_browser`, and the REST + WebSocket protocol are now considered stable; breaking changes follow semver from here.

### What's new since 0.3

* **`ctx.browser.acquire_local(...)`** — first-class primitive for browser flows that need `window.location.hostname` to look like a customer domain (captcha sitekey allowlists, origin-bound OAuth, etc.). Generates a self-signed cert, runs a per-task TLS HTTP server on `127.0.0.1`, injects `--host-resolver-rules`, and yields a `LocalSession` exposing a generic event bus (`session.wait_for(name, timeout)`, `session.events()`). Inherits engine / headless / extra\_args / smart\_router\_preset from the automation's existing `BrowserNeeds`. See [Self-hosted local-host pages](/guides/browser-automations.md#self-hosted-local-host-pages-acquire_local).
* Smaller polish across `SmartRouter` (block-rule precedence), the worker reconcile loop (heal step closes the zero-slot lockout), and the dashboard.

## 0.3.0

* **Browser pool as a first-class declared resource.** Automations declare `Browser = BrowserNeeds(...)` (a class-level ClassVar, same shape as `Pools` / `TaskPayload` / `Output`); the worker bootstrap reads it, pre-warms a shared `BrowserPool` per process, and injects `ctx.browser` for every slot. Per-acquire `SmartRouter` keeps per-task Evomi sticky-sessions working without restarting the Browser. Plugins no longer `import fleet_browser` — see [Browser-based automations](/guides/browser-automations.md).
* **Auto-managed Xvfb via pyvirtualdisplay.** Headed-on-virtual-screen "just works" — `ChromiumWorker.start()` and `slot()` lazily start a managed Xvfb when `DISPLAY` is unset or `:0`. Manual `DISPLAY=:N` (N≠0) still wins.
* **`fleet_browser.BrowserPool` (back).** Pre-warmed pool of async cloak/Playwright workers, `acquire()` context manager, janitor recycles past `max_uses` / `max_age_seconds`. The legacy pool API was dropped during the DrissionPage→Playwright migration; it's now back, adapted to the new async-first ChromiumWorker.
* **`SmartRouter` cost optimisations.** Per-URL family telemetry, content-addressable disk cache for `/xjs/_/` etc., normalised cache keys, block rules for `/gen_204` / `/log` / `/async/` telemetry beacons. Brings a Google SERP from \~1.3 MB to \~180 KB paid wire bytes.

## Earlier development snapshots

Pre-1.0 history (initial master/worker design, the `fleet-core` and `fleet-browser` package consolidation, the DrissionPage → Playwright migration) is in the git log. The framework was used in production on the original Turnstile-solving farm during this period.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://fleet.hackmap.win/meta/changelog.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
