
### 💥 Breaking Changes

- **`llm.model` no longer auto-fills `gemma3:12b`.** Pre-1.7 installs silently downloaded a multi-GB Ollama binary on first use. The field is now empty by default — pick a model in **Settings → LLM**, or research will fail loudly with a clear `ValueError`. Existing users with a saved `llm.model` setting are unaffected; fresh installs see an inline warning in Settings (`aria-live="polite"`) and a clear error on research start. Affects all built-in providers (`openai`, `anthropic`, `ollama`, `lmstudio`, `llamacpp`, `google`, `openrouter`, `ionos`, `xai`). ([#3670](https://github.com/LearningCircuit/local-deep-research/pull/3670))
- **`llamacpp` provider migrated from in-process to HTTP.** No longer loads `.gguf` files in-process via `llama-cpp-python` — connects to `llama-server`'s OpenAI-compatible HTTP endpoint instead. Removed settings: `llm.llamacpp_model_path`, `llm.llamacpp_n_batch`, `llm.llamacpp_n_gpu_layers`, `llm.llamacpp_f16_kv`. New settings: `llm.llamacpp.url` (default `http://localhost:8080/v1`), `llm.llamacpp.api_key` (optional, for auth proxies). Migration: run `llama-server -m <your-model.gguf>` and the default URL picks it up; alternatively use the `openai_endpoint` provider pointed at the same URL. ([#3670](https://github.com/LearningCircuit/local-deep-research/pull/3670))

### 🔒 Security

- Fix LM Studio model detection (#3800) — the settings dropdown was silently dropping models for every auto-discovered provider except Ollama / OpenAI / Anthropic / OpenAI-Compatible-Endpoint, including LM Studio, Llama.cpp, Google Gemini, xAI, OpenRouter, and IONOS. While investigating, also fixed a separate credential-leak class where the route's auto-discovery loop sent every other provider's configured API key in the `Authorization` header to the local LM Studio / llama-server endpoint on every settings-page load. ([#3800](https://github.com/LearningCircuit/local-deep-research/pull/3800))
- Fix SSRF parser-differential bypass (GHSA-g23j-2vwm-5c25). URLs containing
  backslash, whitespace, or ASCII control bytes are now rejected upfront by the
  SSRF validator and notification-URL validator; hostname extraction switched
  from `urllib.parse.urlparse` to `urllib3.util.parse_url` so the validator and
  the HTTP client agree on destination by construction. Credit: @Fushuling,
  @RacerZ-fighting.
- Hardened SSRF defenses against AWS ECS task metadata
  (`169.254.170.2`, `169.254.170.23`), Tencent Cloud (`169.254.0.23`),
  and AlibabaCloud (`100.100.100.200`) metadata endpoints — these are
  now always blocked alongside the existing AWS IMDS / Azure / OCI /
  DigitalOcean entry (`169.254.169.254`). Redacted credentials, path,
  and query from URL-rejection logs (operators with grep/regex tooling
  on the rejection log lines will see authority-only `scheme://host:port`
  instead of full URLs going forward).
- SSRF defense-in-depth: block IPv6 transition prefixes that can wrap
  private IPv4 destinations on hosts with kernel sit0/NAT64 routes.

  - `2002::/16` (6to4, RFC 3056 — deprecated by RFC 7526)
  - `64:ff9b::/96` (NAT64 well-known, RFC 6052)
  - `64:ff9b:1::/48` (NAT64 local-use, RFC 8215 — same SSRF threat class
    as the WKP; missing it earned a HackerOne bounty against
    ssrf_filter)
  - `2001::/32` (Teredo, RFC 4380)
  - `100::/64` (IPv6 discard, RFC 6666)
  - `::/96` (IPv4-Compatible IPv6, RFC 4291 §2.5.5.1 — DEPRECATED 2006;
    same SSRF threat class as the transition prefixes)

  The metadata-IP block is hardened against IPv6-wrapped IMDS access:
  when an IPv6 destination falls in a NAT64 prefix, the embedded IPv4 is
  extracted and matched against `ALWAYS_BLOCKED_METADATA_IPS`, so
  `[64:ff9b::a9fe:a9fe]` cannot reach 169.254.169.254 even on a NAT64
  host.

  Operators on IPv6-only deployments using DNS64+NAT64 (where outbound
  IPv4 traffic is synthesized through `64:ff9b::/96`) can opt back in via
  the env-only setting `security.allow_nat64`
  (`LDR_SECURITY_ALLOW_NAT64=true`). The opt-in is scoped strictly to
  the two NAT64 prefixes — 6to4, Teredo, and discard remain blocked
  unconditionally, and the IMDS embedded-IPv4 carve-out still applies.

### ✨ New Features

- **LM Studio optional API key.** New `llm.lmstudio.api_key` setting for LM Studio instances with authentication enabled. ([#3670](https://github.com/LearningCircuit/local-deep-research/pull/3670))
- **Release notes now use [towncrier](https://towncrier.readthedocs.io) news fragments.** Each PR drops one tiny `changelog.d/<id>.<category>.md` file (categories: `breaking`, `security`, `feature`, `bugfix`, `removal`, `misc`). At release prep, the maintainer runs `pdm run towncrier build --version <X.Y.Z> --yes`, which renders the fragments into `docs/release_notes/<X.Y.Z>.md` and removes them. Replaces the previous shared-file workflow, which scaled poorly at LDR's PR throughput. See `changelog.d/README.md` for the convention. ([#3773](https://github.com/LearningCircuit/local-deep-research/pull/3773))
- **New citation format: source-tagged with global numbering.** Reports can now render citations as `[arxiv-1]`, `[openai.com-2]`, `[arxiv-3]` — the source tag identifies what kind of source each citation is (short URLClassifier tag for known academic sources, cleaned domain for generic web URLs, or the collection name for local RAG/library hits, e.g. `[my-papers-4]`), while the number stays the original global counter so labels never collide and inline citations match the bibliography order. Opt in via `report.citation_format → "Source-tagged with global numbering"`; the existing default remains `Numbers with hyperlinks [1]`. ([#4012](https://github.com/LearningCircuit/local-deep-research/pull/4012))
- Context-overflow warnings (front-page banner, result-page banner, completion toast) now show a clickable action that takes you straight to the relevant diagnostics — historical warnings link to the context-overflow metrics page, while per-research warnings link to that research's overflow section on its details page.
- Context-overflow warnings now include structured fields (`research_id`, `model`, `provider`) and detect both input-only and total-context (input+output) overflow. Four distinct tags (`[provider-confirmed]`, `[total-context]`, `[estimated]`, `[estimated-total-context]`) make different overflow causes distinguishable in logs and alerts. Hosted providers (OpenAI, Anthropic, OpenRouter) get estimation-based detection so their truncation events are no longer invisible. The high-context VRAM warning now links directly to the context-overflow metrics page.
- OpenAI-compatible runtime errors (LM Studio / vLLM / llama.cpp server / OpenRouter / custom endpoint) now surface a message that names the provider, configured base URL, and model. The existing `Error type: <code>` token convention is extended with seven `openai_*` codes (`openai_connection_refused`, `openai_timeout`, `openai_auth`, `openai_permission_denied`, `openai_model_not_found`, `openai_bad_request`, `openai_unknown`), and `ErrorReporter` maps each to the right `ErrorCategory`. The original exception text is preserved in a `Details:` suffix; userinfo embedded in a base URL (`https://u:key@host/v1`) is stripped before display.
- The `/metrics/context-overflow` page is reorganised around per-research truncation diagnosis: redundant aggregate sections (Token Usage Overview, Token Usage Over Time, Model Usage Breakdown, Context Limit Distribution doughnut, Token Usage by Phase) are dropped in favour of empty-state messages and a recoloured and re-shaped scatter chart that buckets points by context-utilisation ratio (green circle / amber triangle / red diamond — shape gives a redundant signal for colourblind users) rather than per-limit dashed reference lines. Tooltips now explicitly label requests with unknown context limits or unknown token counts. The main `/metrics` dashboard gains a small Context Overflow summary panel linking to the full diagnostics page; on aggregation failure the panel shows `—` rather than a misleading `0%`.
- Upload rate limits are now configurable via `LDR_SECURITY_RATE_LIMIT_UPLOAD_USER` and `LDR_SECURITY_RATE_LIMIT_UPLOAD_IP`. The default cap is raised from `10 per minute;100 per hour` to `60 per minute;1000 per hour`, unblocking bulk RAG library uploads.

### 🐛 Bug Fixes

- Fix progress page log panel sizing so it uses CSS layout instead of JavaScript height calculations. ([#2606](https://github.com/LearningCircuit/local-deep-research/pull/2606))
- Progress page current-task text now preserves line breaks and wraps long generated task messages instead of collapsing them into a hard-to-read wall of text. ([#2609](https://github.com/LearningCircuit/local-deep-research/pull/2609))
- Live research log entries now keep the same newest-first visual order as loaded log history instead of appearing at the bottom of the log panel. ([#2610](https://github.com/LearningCircuit/local-deep-research/pull/2610))
- **Journal-quality dashboard ("Your Research") no longer shows orphan papers from deleted research sessions.** Deleting a research session previously left behind `Paper` rows whose `PaperAppearance` had cascade-deleted, so the dashboard kept counting them. The aggregate top-200 query and the predatory-blocked count now exclude papers that have no remaining appearances. Per-research views were already correct. ([#3544](https://github.com/LearningCircuit/local-deep-research/pull/3544))
- Fix the model dropdown in Settings to populate for every auto-discovered LLM provider, not just Ollama / OpenAI / Anthropic / OpenAI-Compatible-Endpoint. LM Studio, Llama.cpp, Google Gemini, xAI, OpenRouter, and IONOS were silently dropped by the frontend (`processModelData`) even when the backend returned their models correctly. ([#3800](https://github.com/LearningCircuit/local-deep-research/pull/3800))
- Fix file descriptor exhaustion (#3816) caused by `ChatOllama._async_client` not being released alongside `_client`. `_close_base_llm()` now closes both the sync and async httpx clients owned by `ChatOllama`, addressing the eventpoll-FD growth seen in long-running deployments. ([#3816](https://github.com/LearningCircuit/local-deep-research/pull/3816))
- Fix the research details "View Journals" button so it opens the scoped journal-quality dashboard instead of a missing page. ([#3825](https://github.com/LearningCircuit/local-deep-research/pull/3825))
- Fix Download Manager failure after 20–30 PDFs with `UNIQUE constraint failed: documents.document_hash` and the cascading PendingRollbackError that prevented further downloads. ([#3827](https://github.com/LearningCircuit/local-deep-research/pull/3827))
- Fix login/registration over `http://localhost:5001` on Docker Desktop.
  The session cookie's `Secure` flag is now driven purely by the request
  protocol (HTTPS) rather than by source-IP heuristics, so HTTP requests
  from any IP — including Docker Desktop's NAT gateway — now keep their
  cookies and CSRF tokens. ([#3849](https://github.com/LearningCircuit/local-deep-research/pull/3849))
- Fix Ollama embedding indexing aborting with `"input length exceeds the context length"` (HTTP 500) by exposing a configurable `embeddings.ollama.num_ctx` setting. The new field appears on the embedding settings page when the Ollama provider is selected and ships with a default of 8192 tokens, which covers chunk sizes up to ~5000 characters for `nomic-embed-text`, `mxbai-embed-large`, `bge-m3`, and `qwen3-embedding`. ([#3870](https://github.com/LearningCircuit/local-deep-research/pull/3870))
- LangGraph agent strategy error messages now include the exception type, and the `'str' object has no attribute 'model_dump'` failure (issue #3897) is translated into a rich on-result-page recommendation listing the three possible causes — proxy/shim, serving stack, or model — with concrete remediation steps for each. ([#3926](https://github.com/LearningCircuit/local-deep-research/pull/3926))
- The Context Overflow summary panel on `/metrics` now honors the dashboard's research-mode filter (Quick / Detailed / All) the same way the rest of the panels do.

### 📝 Other Changes

- Added `LDR_DISABLE_RATE_LIMITING` as the canonical name for disabling HTTP rate limiting, matching the `LDR_` env-var convention. The legacy `DISABLE_RATE_LIMITING` still works but emits a deprecation warning. Note: this is distinct from `LDR_RATE_LIMITING_ENABLED`, which controls the adaptive search-engine rate limiter — a different subsystem (#3905).
- Migrated CI workflows to the canonical `LDR_DISABLE_RATE_LIMITING` env var (added in #3936) and closed a test-isolation gap in `test_enabled_by_default` that did not clear the canonical var.
- Removed unused chart-rendering JavaScript on the context-overflow analytics page (token-usage-over-time stacked bar, model-token-stats table, context-limits-distribution doughnut, and the configured-limits list). These functions stopped being called when the page was reorganised to focus on truncation diagnosis (#3792); deleting them now drops ~200 lines of dead code without any user-visible change.
- The prerelease Docker workflow now also re-points the floating `prerelease` tag at each new RC manifest. Testers can pin `image: localdeepresearch/local-deep-research:prerelease` in their compose file and run `docker compose pull && docker compose up -d` to fetch the latest RC without editing the tag each cycle. Versioned tags (`prerelease-vX.Y.Z-<sha>`) are still published for reproducible testing.
