# Changelog All notable changes to AgentCrawl Community are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Each entry gives a one-line "what changed" up front, then the engineering detail for anyone who wants to verify the fix landed. ## Unreleased ## 0.7.0 - 2026-10-25 A full review of 0.6.0: an SSRF read-back in the default browser mode, crashes on common HTML, jobs stuck after a restart, `/v2` options that failed or were dropped, fewer thin pages, and docs that promised more than the code did. Breaking: some requests that used to pass silently are now refused, and servers sharing a database must restart together (see "Upgrading from 0.6"). ### Upgrading from 0.6 - **Restart every server process that shares the database together.** An older process does not know the new run token and would requeue jobs a new one is running. - An unknown format (including `raw_html`) raises `ValueError` in the library, `422` in the API and a usage error in the CLI, instead of an empty document. - `/v2` refuses options it cannot honour instead of dropping them: `scrapeOptions.formats` other than markdown/html/links or `onlyMainContent: false` on crawl, `sitemap` other than `include` on map, `zeroDataRetention` on crawl and batch. - API `config` overrides above the new caps are refused (`crawl_max_pages` 10,000, `max_response_bytes` 50 MB, `timeout_ms`/`page_budget_ms` 5 min, `page_budget_ms` 0, `browser_wait_ms` 60 s, retry delays 60 s, `http_retries`/`crawl_url_retries` 5, more than 20 `include`/`exclude` patterns), and so are request bodies over 1 MB (`AGENTCRAWL_MAX_REQUEST_BYTES`), with `413`, also when the body is sent chunked. - `crawl()` no longer crawls a site whose `robots.txt` answers 5xx/429 or does not answer: pass `--no-robots` if you must. - `agentcrawl doctor` exits 1 when a check fails, and `doctor --client` exits 1 for a stale or unreadable client, or a named client that is not registered: scripts that ignored its output may now fail. - **The server now reads the engine variables local mode reads** (`AGENTCRAWL_AIRGAP`, `AGENTCRAWL_AUDIT`, `AGENTCRAWL_PAGE_BUDGET_MS`, `AGENTCRAWL_BROWSER_ENGINE`, `AGENTCRAWL_OCR`, retries, crawl defaults): check the server's environment, for example a `.env` shared with the MCP server. With `AGENTCRAWL_AIRGAP` set, it refuses every target outside the allowlist. - `summary` counts against `AGENTCRAWL_LLM_MAX_PAGES` (default 20), like `json`: a batch, or a `/v1/search` with `scrape`, asking for more summaries than that is refused. - A remote base URL with `user:password@`, or without `http://`/`https://`, is refused at startup: pass the key as `AGENTCRAWL_API_KEY` or `--api-key`. - `regex` is a new core dependency; the `server` extra needs `starlette>=1.3.1`/`fastapi>=0.133.0` and the `mcp` extra `mcp>=1.30.0`. ### Changed - **A cancelled job always keeps the pages already scraped.** A cancel that landed while the job waited between quanta, or that the recovery sweep finished, deleted them; only a cancel during a run kept them. The job's result now lists them (`metadata.cancelled: true`) and they are recorded as usage for the job's key; a job cancelled before any page still has no result. Its failed URLs are kept in the failures list too. - **Crawl jobs bill each page once:** a failures retry or a cancel no longer bills again the pages an earlier run of the job already paid for (the job now records how many pages it billed). - **Job heartbeat survives a transient database error** (`database is locked`): the renewal thread used to die on it, so the lease of a live worker expired and a peer ran the same job again. - **A stalled worker is fenced out of its job:** each run claims the job with its own token, and its lease renewals, progress, checkpoints, requeues and final result are refused once a successor has recovered and claimed the job. The stalled run stops between pages instead of finishing (and billing) the job a second time. - **A cancelled job that waits for a delayed retry ends at once** (`DELETE /v1/jobs/{id}`, `/v2` crawl and batch cancel): it stayed `cancelling` until its retry timer fired, up to 30 minutes. Any job no run is executing is now cancelled by the request itself, and `/v1` answers `cancelled` in that case instead of `cancelling`. Internally the run's lock (`run_token`, `run_expires_at`) is separate from the scheduling lease of a queued job; existing databases move a running job's lock to the new columns on start. Processes that share the database migrate one at a time, and a job claimed by a process from before this change stays alive while its old lock is. **Upgrade note:** still restart every server process that shares the database together: an older process does not know the run token and would requeue jobs a new one is running. - **`crawl()` follows RFC 9309 when `robots.txt` cannot be read:** a server error (5xx, or 429) or a network failure, after one retry, now blocks the crawl ("robots.txt unreachable, crawl disallowed") instead of crawling everything; a 4xx, a redirect loop or an unusable body still means no rules. `--no-robots` overrides it. - **TypeScript SDK license is Apache-2.0**, the same as the Python package (it said MIT). - **Benchmark wording:** a gap that is not significant is written "no significant difference", not "tie" (a test cannot show two tools are equal). - **Unknown formats are refused** (library `ValueError`, API `422`, CLI usage error) instead of returning a document with nothing in it. `raw_html` was never supported (the TypeScript `Format` type listed it). - **API limits:** request bodies over `AGENTCRAWL_MAX_REQUEST_BYTES` (1 MB) are refused before authentication; `config` overrides are capped at `crawl_max_pages` 10,000, `max_response_bytes` 50 MB, `timeout_ms`/`page_budget_ms` 5 min (`page_budget_ms` 0 refused), `browser_wait_ms` 60 s, retry delays 60 s and `http_retries`/`crawl_url_retries` 5; at most 20 `include`/`exclude` patterns. - **`regex` is a core dependency**: `include`/`exclude` matches are limited to 50 ms each. - **`agentcrawl doctor`** exits 1 when a check fails; `doctor --client` exits 1 for a stale or unreadable client, or a named client that is not registered. With local files disabled (the Docker image) its scrape check runs on HTML in memory instead of failing. - **The server reads every documented `AGENTCRAWL_*` engine variable** (it ignored `AGENTCRAWL_BROWSER_ENGINE`, `AGENTCRAWL_PAGE_BUDGET_MS`, `AGENTCRAWL_AIRGAP`, `AGENTCRAWL_AUDIT`), except the `cdp` engine, which stays local-mode only (the server ignores `AGENTCRAWL_BROWSER_ENGINE=cdp` and `AGENTCRAWL_CDP_URL`), and local mode reads the Camofox, retry and crawl variables the server documented. - **`/v2`:** options the engine cannot honour are refused instead of dropped (`scrapeOptions.formats` other than markdown/html/links on crawl, `onlyMainContent: false` on crawl, `sitemap` other than `include` on map); `storeInCache: false` and `zeroDataRetention: true` skip the cache, and `zeroDataRetention` is refused on crawl and batch. - `metadata.dns_pinned` is `false` whenever an HTTP(S) proxy is configured in the environment. ### Security - **Deeply nested HTML** (40,000 nested `
`, 200 KB) took 25 s per parse after the implied-end-tag fix below, and thousands of end tags with nothing to close were quadratic too; open elements are now indexed by tag, so both lookups take logarithmic time and give the same tree as before. - `/v1/search` with `scrape` and `json`/`summary` formats checks `AGENTCRAWL_LLM_MAX_PAGES` before searching; it sent every result page to the operator's LLM. - **SSRF read-back through iframes (default browser mode).** Outside `browser_strict_network` Chromium follows an iframe's redirects itself; a public frame redirecting to a loopback, private or metadata address had that page's HTML copied into the Markdown. Frames (and the page) whose URL the guard refuses when their HTML is read are no longer returned. The request itself still happens outside strict mode (SECURITY.md). - NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`), 6to4 and IPv4-compatible IPv6 addresses are checked by the IPv4 address they carry. - Page text and HTML sent to the LLM (extraction, schema generation, summaries) are fenced as untrusted data. - **ReDoS:** an `include`/`exclude` pattern such as `(a|aa)+$` froze the whole server (Python's `re` holds the GIL while it backtracks). Matching now times out; a timed-out exclude still excludes. The `regex` fields of CSS schemas (`/v1/extract_css`, MCP `extract_structured`) have the same 50 ms limit; a timed-out field is `null`. - Hidden text no longer reaches the Markdown when hidden by a valueless `hidden` attribute or `display:none !important` (`hidden="until-found"` stays: it is collapsed content). - URL credentials (`user:pass@`, e.g. a proxy) are redacted from error messages. - **A remote base URL with `user:password@`, or without `http://`/`https://`, is refused** with a clear message: `AgentCrawlClient(...)` raises `ValueError`, `--remote` commands exit with it, the MCP server stops at startup and `agentcrawl doctor` reports it. urllib cannot send URL credentials, so such a URL never worked, and its error printed the password (`doctor` also showed the URL as is). Pass the key as `AGENTCRAWL_API_KEY` or `--api-key`. - URL credentials are also redacted from `/v1/scrape_many` per-URL errors, `/v1/search` failures (API and MCP) and the error stored and served for a failed crawl job. A job's error is no longer cut to 300 characters (up to 4,000, one line), names the exception type when it has no message, and is logged with the traceback's frames but only the redacted message. - A bearer token with non-ASCII characters is refused with `403` instead of crashing the key check (`500`). - **Dependency floors:** the `server` extra requires `starlette>=1.3.1` (CVE-2026-54282, CVE-2026-54283) and so `fastapi>=0.133.0`; the `mcp` extra requires `mcp>=1.30.0`. A new workflow audits every runtime extra with `pip-audit` on each push, pull request and weekly. ### Fixed - `agentcrawl restore --force` on a database that is locked or read-only (the server still has it open) stops with a message instead of deleting the live database and its WAL. - A crawl blocked because `robots.txt` was unreachable records its pages as retryable failures (an outage, not the site's rules), and a `robots.txt` whose body stalls past the read deadline counts as unreachable instead of "no rules". - The recovery sweep no longer queues a second copy of a delayed retry that is waiting for a free worker. - Upgrading a database from 0.6.0 no longer marks failed jobs as billed: they never were, so their pages are billed when a retry finishes them. - Pages over 1 MB skip the extraction cache, which kept up to four whole pages in memory for the life of the process. - **`metadata.language` and `/v2` `ogImage` are filled:** the language comes from the first `` and `og:image` is kept; neither was ever set, so `/v2` scrape answers never carried Firecrawl's `metadata.language` or `metadata.ogImage`. Pages cached before the upgrade lack them until they expire. - **The MCP server reports AgentCrawl's version** in `initialize` (`serverInfo.version`); it answered the MCP SDK's version (1.27.2). - **MCP `search_web` takes `formats`** like `scrape_url`, so an agent can ask for a `summary` of each result; `scrape_url` now lists `summary` among its formats (it worked but was not described). - **`summary` counts against `AGENTCRAWL_LLM_MAX_PAGES`** like `json` (one LLM call per page): a batch, crawl or search asking for more summaries than the cap is refused before any call. - `.hermes/` (local agent state) is in `.gitignore`, not only in a local exclude a fresh clone does not have. - **HTML with omitted end tags** (`
  • a
  • b`, `12`, `

    a

    `) nested one level per item: a list of ~400 items raised `RecursionError` out of `scrape()` and aborted the crawl. Implied end tags are closed as browsers do, in the extractor and in CSS extraction (`ul > li`, `tr > td` now match); deeper markup falls back instead of crashing. - **Links** resolve against the final URL after redirects (also in the browser) and the page's ``, the same in `links`, the Markdown and CSS extraction; `metadata.source_url` stays the URL you asked for. - **A page rescued by the browser retry** takes the normal path: `section`, `max_tokens`, `json`, `summary` and `markdown_sha256` were skipped (and `diff()` raised). - **Sitemaps:** one broken child no longer drops the whole index (it is reported in `errors`); gzipped sitemaps are read. - A `robots.txt` starting with a UTF-8 BOM had its first group (often `User-agent: *`) ignored. - `` on an ASCII page is read as UTF-8; every member of a multi-member gzip body is kept. - **Crawl jobs:** the running worker renews its lease on a timer and a sweep every 60 s requeues jobs whose worker died, so a crash and quick restart no longer leaves a job `running` (or `cancelling`) forever; a duplicate queue entry can no longer drop the runner's lease. Re-saved documents keep their place, and documents saved together keep crawl order. - An idempotent `POST /v1/crawl` sent again after a failure retry no longer gets `409`. - `/v2/crawl` `includePaths`/`excludePaths` were rejected with `400` (regexes wrapped in `*`); anchored Firecrawl path patterns (`^/blog/`) now match. Bad or out-of-range numbers on `/v2` are `400`, not `500`; running `/v2` jobs page past 100 documents. - `agentcrawl restore --force` no longer replays a stale WAL over the restored file and replaces a corrupt target; `failures --export` writes the CSV in remote mode. - TypeScript SDK: `outline` format, `scroll_to_end` / `virtual_scroll` actions, `section` and `maxTokens` on `scrape()`. - **Fewer thin pages:** the text count behind the thin-page check read utility-CSS class names (`[&_.card>*]:h-full`) as page text, so a thin page looked long enough and was never rendered in the browser. A `
    ` inside `
    ` or `
    ` (a section heading, not the site header) is kept unless it is mostly links. A page with `