---
summary: "Generalize the /observations preflight from germplasm-only to any unanchored query, and make the probe fail-soft via companion call options so a stalled count surfaces as a warning instead of a hard error."
breaking: false
---

# 0.4.8 — 2026-05-01

The v0.4.6 / v0.4.7 fixes routed the right *singular* filters to CassavaBase. v0.4.8 closes the remaining hole on the matching tool: even with the right filter names on the wire, an unanchored `/observations` query still triggers a full join and stalls past the timeout. v0.4.7's preflight only covered the germplasm-only shape; field-testing surfaced the same blow-up on variable-only and bare-query shapes (verified with `curl --max-time 60` against `cassavabase.org/brapi/v2`: `?observationVariableDbId=…` and bare `/observations` both never respond). On top of that, the preflight itself ran on the global retry budget — when the count probe stalled, the agent waited the full 4 × 30 s before getting any signal back.

This release generalizes the preflight to **every unanchored shape** (no `studies` / `trials` / `observationUnits` / `observations` scope), routes the probe through `companionRequestOptions` (8 s timeout, 0 retries), and treats probe failure as a soft skip — the agent gets a single "narrow your query" warning instead of a 2-minute hang or a propagated error.

## Added

- **`probeObservationCount(...)`** in `brapi-find-observations.tool.ts` — pulls a 1-row sample on `/observations` with companion-call shape (short timeout, zero retries) so a stalled upstream count operation surfaces as `null` instead of throwing. The handler converts that to a "preflight count probe stalled" warning and skips the bulk pull entirely.
- **Three new specs** on `brapi_find_observations`:
  - variable-only queries trigger the preflight and bail above the threshold (locks in the symmetry with germplasm-only).
  - bare `/observations` queries (no filters at all) trigger the preflight.
  - probe-failure path emits a "stalled" warning, returns an empty result, and skips the bulk pull instead of throwing.

## Changed

- **`brapi_find_observations` preflight trigger** generalized from `!hasScope && germplasm.length > 0` to `!hasAnchor` — i.e. *any* unanchored shape (germplasm-only, variable-only, season-only, programs-only, bare). The four anchors (`studies`, `trials`, `observationUnits`, `observations`) still skip the preflight as before; the common, well-anchored case pays no extra HTTP.
- **Preflight probe** no longer reuses `loadInitialFindPage` (which threads the global retry budget). It now calls `BrapiClient.get` directly with `companionRequestOptions(...)` so the wall-clock budget is bounded by `BRAPI_COMPANION_TIMEOUT_MS` (8 s default) and a stalled count operation can't pin the response.
- **Three pre-existing specs** that exercised data-handling paths via bare `{}` input now anchor with `studies: ['s-1']` so the test still reads as one logical fetch (it was previously coincidentally one HTTP call; with the broader preflight that test would now consume the same `Response` body twice and fail with `ERR_BODY_ALREADY_USED`). The behavior under test is unchanged.

## Fixed

- **CassavaBase variable-only `/observations` queries** no longer hang. Before: `?observationVariableDbId=X` (alone) issued, upstream stalled past 60 s, retry budget escalated to ≥ 120 s, agent eventually got a hard timeout. After: preflight fails fast under 8 s, agent gets a "narrow your query — add a `studies` / `trials` / `observationUnits` anchor" warning and an empty result.
- **CassavaBase bare `/observations` queries** (no filters) no longer hang. Same shape as above — verified `curl` against the live server times out at 30 s+ on the bare endpoint.
- **`brapi_find_observations` total wall time** on stalled unanchored shapes drops from ≥ 60 s (or longer if retries kicked in) to ≤ 8 s + a single warning.
