> 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/concepts/configs-and-reconcile.md).

# Configs and reconcile

Every worker has a config. The master stores it. The worker observes it. Reconcile is the function that turns the difference between observed and desired into a plan.

## BaseConfig

Every plugin's config is a Pydantic model subclassing `BaseConfig`:

```python
class BaseConfig(BaseModel):
    enabled: bool = False
    slots: int = Field(default=0, ge=0, le=1024)
    recycle_seconds: int = Field(default=0, ge=0, le=86400)
    max_attempts: int = Field(default=3, ge=1, le=100)
    visibility_seconds: int = Field(default=300, ge=10, le=86400)
    proxy_url: str | None = None
    proxy_provider: str | None = None
    proxy_provider_config: dict | None = None
```

These fields are common to every automation. The framework reads them directly. Your plugin adds whatever else it needs.

`max_attempts` and `visibility_seconds` only matter for `BatchAutomation`. `max_attempts` caps retries before a task lands in the DLQ. `visibility_seconds` is how long a reserved task is hidden from other slots; if the worker crashes before ack, the master's sweeper re-queues the task after this many seconds. The per-task submit fields (`max_attempts`, `ttl_seconds`, `priority`, `idempotency_key`) override or supplement these — see the [batch automation guide](/guides/batch-automation.md).

`proxy_url` is the simple-case proxy: one URL, used as-is. `proxy_provider` plus `proxy_provider_config` is the extensible path — see [proxy providers](/guides/proxy-providers.md) for the full system. When both are set, the provider wins.

```python
class CaptchaConfig(BaseConfig):
    sitekey: str
    target_url: str
    widgets_per_slot: int = 2
```

## Config gen

Every config update bumps an integer `config_gen` stored in Redis at `wkr:<type>:<id>:gen`. Workers receive the gen with every config payload and refuse to apply anything with `gen <= current_gen`. This prevents two failure modes:

A retried PATCH from a chatty admin client doesn't cause two restarts.

A master restart that resets gens to zero doesn't make a running worker tear itself down.

The trade-off: if the master loses its Redis state, you need to PATCH each worker a few times until the master's new gen exceeds the worker's cached gen. There's a [troubleshooting](/operations/troubleshooting.md) entry for this exact case.

## The reconcile diff

`fleet.core.reconcile.diff(observed, desired)` returns a list of `Action` objects describing the smallest plan that converges observed to desired. The list is usually one action long, but a slot-count change combined with a restart-class field change produces two — the scale plus a page recycle (see below). The rule table, in priority order:

| Condition                                          | Action          |
| -------------------------------------------------- | --------------- |
| `desired.enabled == False`                         | `stop_all`      |
| `observed.enabled == False` and desired is enabled | `cold_start`    |
| Any reboot-class field changed                     | `restart`       |
| `desired.slots > observed.slots`                   | `scale_up`      |
| `desired.slots < observed.slots`                   | `scale_down`    |
| Any restart-class field changed                    | `restart_pages` |
| Otherwise                                          | `noop`          |

The rules are checked top to bottom. A reboot-class change short-circuits to a single `restart` — the most disruptive action wins and nothing else is needed, since a full worker restart already picks up the new slot count. Scale and `restart_pages` are the exception: they compose. Change both the proxy (a restart-class field) and the slot count and you get a two-action plan `[scale_up, restart_pages]` — the scale carries the new config so freshly spawned slots start with the new proxy, and `restart_pages` recycles the pre-existing slots onto it. Neither the new proxy nor the new count is dropped.

## Reboot vs. restart fields

A plugin declares which fields trigger which action with two classmethods on its `BaseConfig` subclass:

```python
class CaptchaConfig(BaseConfig):
    sitekey: str
    target_url: str
    widgets_per_slot: int = 2
    humanizer: bool = True

    @classmethod
    def reboot_fields(cls):
        return frozenset({"sitekey", "target_url"})

    @classmethod
    def restart_fields(cls):
        return frozenset({"widgets_per_slot", "humanizer"})
```

`reboot_fields` trigger a `restart` action (full cold-start, including the worker-level resources like a local token server). Use this for fields that change worker-level setup, not just per-slot behavior.

`restart_fields` trigger a `restart_pages` action (every slot recycled, but worker-level resources stay). Use this for fields that change per-slot behavior — page URLs, click cadences, headers.

`BaseConfig` itself adds `proxy_url` to `restart_fields` by default. A proxy change always recycles slots because the browser or HTTP client is bound to the proxy at startup.

If you don't override these, the framework only knows about scale changes and enable/disable. Other field changes will appear as a `noop` — the field changes in Redis but no slot is restarted to pick it up. This is a common source of confusion.

## The recycle loop

Independent from the reconcile loop. Runs every few seconds inside each worker. Two responsibilities:

If any slot has been alive for longer than `recycle_seconds`, tear it down and the heal step (next item) will respawn it.

If `len(slots) < desired.slots`, spawn the missing ones.

The heal step matters more than the recycle step. Recycle is a quality-of-life feature: long-lived browser sessions get fingerprint-score creep, so rotating them every 10 minutes keeps the success rate up. Heal is a correctness feature: without it, a slot that fails to start (because of resource exhaustion or a transient browser crash) stays dropped forever, because the reconcile diff only fires on config changes, not on state divergence.

The heal step is the reason a worker can lose all its slots and recover automatically. It's also why the `fleet-browser` pool doesn't need to retry-on-failure inside its own code: failure just means "less than desired", and the next tick will try again.


---

# 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/concepts/configs-and-reconcile.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.
