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 discovery

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Treat as flow control: record items as discovery-skipped and let the run finish (COVERAGE_PARTIAL is the designed outcome)
  2. Fix the underlying Exa condition — the '[search:provider-cooldown]' warn fired when the gate opened carries the last error detail
  3. Wait out the skip window before re-running; the half-open probe tests recovery
  4. 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

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


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/0a06eddc25d8aad7. Report an issue: GitHub.