koala73/worldmonitor · warning · SearchTargetError
Exa discovery cooldown is open for "${canonicalName}"
Error message
Exa discovery cooldown is open for "${canonicalName}" What it means
The Exa discovery cooldown gate (exaDiscoveryGate) opened after consecutive Exa search failures, and because Exa is the sole URL discovery provider, the target aborts immediately with failures [{provider:'exa', reason:'provider-cooldown'}] and durationMs 0. Unlike the Firecrawl gate, this abort is intentional coverage loss: without Exa there are no candidate URLs to extract from, so failing fast beats hammering a dead provider. The comment notes an Exa extraction cooldown must NOT abort this way — only discovery does.
Source
Thrown at consumer-prices-core/src/adapters/search.ts:669
// probe (the probe call itself happens inside _extractFromUrl).
if (ctx.config.searchConfig?.extractionFallback !== 'exa' && this.firecrawlGate.consumeSkip()) {
throw new SearchTargetError(
`Firecrawl extraction cooldown is open for "${canonicalName}"`,
0,
[{ provider: 'firecrawl', reason: 'provider-cooldown' }],
);
}
// Only the DISCOVERY cooldown can abort the target: Exa is the sole URL
// discovery provider, so without it there is nothing to extract from. An
// Exa *extraction* cooldown must not abort — Firecrawl is the primary
// extractor and is frequently healthy at that moment (its own streak resets
// on every success), and `_extractFromUrl` already skips the cooled-down
// provider per candidate. Aborting here would turn a fallback outage into
// a whole-basket loss, which is the COVERAGE_PARTIAL this adapter exists
// to prevent.
if (this.exaDiscoveryGate.consumeSkip()) {
throw new SearchTargetError(
`Exa discovery cooldown is open for "${canonicalName}"`,
0,
[{ provider: 'exa', reason: 'provider-cooldown' }],
);
}
const marketName = MARKET_NAMES[ctx.config.marketCode] ?? ctx.config.marketCode.toUpperCase();
const cfg = ctx.config.searchConfig;
const discoveryRequest = buildExaDiscoveryRequest({
searchConfig: cfg,
canonicalName,
category: target.category,
currency,
marketName,
includeDomains: hostAllowlist,
});
// Stage 1: Exa URL discoveryView on GitHub (pinned to eeab0a219f)
Solutions
- Treat as flow control: record items as discovery-skipped and let the run finish (COVERAGE_PARTIAL is the designed outcome)
- Fix the underlying Exa condition — the '[search:provider-cooldown]' warn fired when the gate opened carries the last error detail
- Wait out the skip window before re-running; the half-open probe tests recovery
- Reduce Exa pressure (concurrency, query volume) if 429s keep re-opening the gate
Example fix
// before — cooldown aborts counted as coverage failures const failed = results.filter((r) => r.error); // after — exclude provider-cooldown aborts from coverage math const isCooldown = (r) => r.failures?.some((f) => f.reason === 'provider-cooldown'); const failed = results.filter((r) => r.error && !isCooldown(r)); const coverage = parsed / (total - results.filter(isCooldown).length);
Defensive patterns
Strategy: try-catch
Type guard
function isExaDiscoveryCooldown(err: unknown): boolean {
return err instanceof SearchTargetError
&& err.failures.some((f) => f.provider === 'exa' && f.reason === 'provider-cooldown');
} Try / catch
try {
await adapter.fetchTarget(ctx, target);
} catch (err) {
if (isExaDiscoveryCooldown(err)) {
stats.discoverySkipped++; // expected under an Exa outage — COVERAGE_PARTIAL by design
continue;
}
throw err;
} Prevention
- Alert on the gate-opening warn line, not on every per-item cooldown abort
- Back off re-runs while the gate is open — immediate re-runs defeat the half-open probe
- Track how often exaDiscoveryGate opens: recurring opens point at quota or concurrency, not luck
When it happens
Trigger: Two or more consecutive Exa discovery failures (HTTP errors, timeouts) open exaDiscoveryGate; each target inside the skip window throws this at entry; a failed probe re-opens the gate for another window.
Common situations: An Exa rate limit or outage during basket discovery — the run logs one provider-cooldown per item and finishes COVERAGE_PARTIAL by design; operators mistake these for per-item errors and immediately re-run, which keeps hammering the dead provider.
Related errors
- Firecrawl extraction cooldown is open for "${canonicalName}"
- Exa search failed for "${canonicalName}": ${detail}
- Exa returned no content for ${url}
- Exa returned malformed structured summary for ${url}
- Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${d
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/0a06eddc25d8aad7.
Report an issue: GitHub.