import type { SearchEngine, SearchEngineOptions, RawSearchResult } from '../../types.js'; /** * Quality tier for an engine adapter. Reflects observed snippet quality + * stability of the upstream source. The tier is consumed to weight RRF * fusion — higher-tier engines contribute more to the fused ranking. * * Tier semantics (see also docs in src/search/core/engine-quality.ts): * - 'high' : authoritative source with structured payload (JSON/API), * stable schema, rich snippets. Example: StackOverflow API, * Wikipedia OpenSearch, MDN docs API. * - 'medium' : scraped HTML or a structured feed where snippets are * useful but can be thin or noisy. Example: Bing, DDG Lite, * Brave web (description short), HN Algolia (points/comments * fallback snippet), arXiv, Semantic Scholar (abstract may * be missing). * - 'low' : sparse / boilerplate snippets, or a curated lookup that * returns mostly metadata rather than evidence text. Example: * devdocs (static slug table, no body content), lobsters * (often returns "N score / N comments" rather than evidence). */ export type EngineQualityTier = 'high' | 'medium' | 'low'; export interface EngineEntry { engine: SearchEngine; /** Optional weight for downstream RRF/scoring. Default 1. */ weight?: number; /** Whether this engine accepts date filters in options.fromDate/toDate. */ supportsDateFilter?: boolean; /** Marks an engine as a low-priority secondary signal. Results that * were contributed only by secondary engines are demoted when their * lexical alignment with the query is low. Used by the code vertical * to admit MDN without letting it dominate database/library queries. */ secondary?: boolean; /** Snippet / source-quality tier, consumed to weight RRF fusion. Every * registered entry MUST set a tier; a registered-engines test enforces * that the field is present. */ quality?: EngineQualityTier; /** When true, the engine is registered but the orchestrator must skip * dispatch. Used when an upstream endpoint is gone or the adapter is * intentionally parked pending a rewrite — the slice spec calls this * out as a soft-disable so the adapter file isn't deleted (CEO call). */ disabled?: boolean; /** When true, the engine is NOT dispatched in the primary wave — it is held * back and dispatched only by the orchestrator's degraded-recovery wave (see * orchestrator.ts) when the primary pool collapses below the health floor. * Used for an engine that is a per-call latency/failure tax on the happy path * (e.g. a source that reputation-blocks this network most of the time) but * still contributes an independent lexical signal when the pool is starved * and needs every engine it can get. Generic — no engine name is inspected * by the dispatch logic; the roster decides via this flag. */ probeOnly?: boolean; } export interface EngineOutcome { engine: string; ok: boolean; results: RawSearchResult[]; error?: string; latencyMs: number; /** True when the breaker tripped and we skipped the call. */ skipped?: boolean; /** Remaining breaker cooldown in ms, set only when skipped. */ cooldownRemainingMs?: number; /** True when the engine was still in flight at the pool's soft deadline * (or its tighter chronic budget) and was abandoned so a straggler could * not drag the overall response. Its underlying request keeps running and * its own abort timeout still fires; a late result may populate cache but * is not awaited. */ timedOut?: boolean; } /** Options for {@link runEnginesParallel} that bound how long the pool waits. */ export interface RunEnginesOptions { /** Overall soft deadline in ms. Once elapsed, engines still in flight are * recorded as `timedOut` outcomes and no longer awaited. Undefined = * legacy Promise.all behaviour (wait for the slowest engine). */ softDeadlineMs?: number; /** Tighter per-engine soft deadline applied ONLY to engines whose session * trip count is at/above the chronic threshold. Lets the pool stop paying a * chronically-failing engine's straggler cost every call while a healthy or * transiently-slow-once engine keeps the full pool deadline. Generic and * data-driven — keyed on observed session trips, never an engine name. */ chronicSoftDeadlineMs?: number; } export interface BreakerConfig { /** Fail count to trip. Default 3. */ failureThreshold?: number; /** Cooldown after tripping, ms. Default 60_000. */ cooldownMs?: number; /** In-call retry attempts before the breaker records a failure. Default 2 * (one retry). The inter-attempt backoff grows exponentially from the base * so a rate-limited engine is not hammered. */ retryAttempts?: number; /** Minimum inter-request interval (ms) for a rate-limit-prone engine. A call * arriving within this window of the previous dispatch is SKIPPED (throws * {@link ThrottledError}) rather than waiting — waiting would poison the * pool's soft deadlines and serialize multi-query fan-out. When omitted (and * no name-registered default exists) the engine is never throttled. * A per-engine default can be pre-registered via * {@link registerEngineMinInterval}; the explicit option wins over it. */ minIntervalMs?: number; } /** * An engine that opts into the retry loop's rotation hook. The base * `SearchEngine` contract is unchanged — this optional method lets an * HTML-scraping adapter react to a retryable error (e.g. rotate its browser * fingerprint on a 403) before the next attempt. The retry loop calls it * only between attempts, never after the final one. */ export interface RetryableEngine extends SearchEngine { onRetry?(attempt: number, lastError: unknown): void; } /** Session trips at/above this count mark an engine as chronically unhealthy. * The pool then applies the tighter `chronicSoftDeadlineMs` budget to it so a * repeatedly-failing engine stops draining wall-clock every call. A trip * happens at most once per cooldown window, so this many trips means the * engine has failed across several distinct recovery attempts — not a one-off * blip. Generic + data-driven; no engine name is special-cased. */ export declare const CHRONIC_TRIP_THRESHOLD = 3; /** Marginalia's minimum inter-request interval. It rate-limits (429) aggressively * under a burst; spacing calls at least this far apart keeps it in the pool * instead of tripping its breaker. Generic mechanism — the value is registered * against the engine name, not special-cased in the dispatch logic. */ export declare const MARGINALIA_MIN_INTERVAL_MS = 2000; /** Register a default minimum inter-request interval for an engine by name. * Idempotent. An explicit `minIntervalMs` on wrapWithRetryAndBreaker wins. */ export declare function registerEngineMinInterval(name: string, ms: number): void; export type FailureClass = 'rate-limit' | 'forbidden' | 'other'; /** Classify an engine failure by its error text. `rate-limit` (429 / "rate * limit" / "too many requests") is transient; `forbidden` (403 / "forbidden") * is a reputational block; everything else is `other`. Pure + engine-agnostic. */ export declare function classifyFailure(err: unknown): FailureClass; /** Clear ALL breaker state (failures, cooldowns, trips, sessionTrips). Public: * doctor `--fix` and the daemon admin reset route call this to un-stick an * engine pool that collapsed under a burst. Also the reset used by tests. */ export declare function resetBreakers(): void; /** Delegating alias kept for the many test files that import it. Renaming * would be pure churn (and would collide with parallel work), so the public * name is `resetBreakers` and this stays a reference to the same function. */ export declare const _resetBreakersForTest: typeof resetBreakers; /** * Cumulative breaker trips for an engine over the life of this process. * Unlike the per-cooldown `trips` counter, this does NOT reset when the * engine recovers — so a flaky engine that trips, recovers, and trips again * is recognised as chronically unhealthy. Pure read; 0 for an engine that * has never tripped (or never dispatched). */ export declare function getEngineSessionTrips(name: string): number; /** True when an engine has tripped enough times this session to be treated as * chronically unhealthy (see {@link CHRONIC_TRIP_THRESHOLD}). */ export declare function isEngineChronicallyUnhealthy(name: string): boolean; export type BreakerSnapshotState = 'closed' | 'open' | 'half-open'; export interface BreakerSnapshotEntry { engine: string; state: BreakerSnapshotState; failures: number; cooldownRemainingMs: number; lastError?: string; } /** * Point-in-time view of every breaker that has seen at least one call. * `half-open` = cooldown elapsed but the breaker has not closed yet (probe * pending or in flight). Pure read — never mutates breaker state. */ export declare function getBreakerSnapshot(): BreakerSnapshotEntry[]; export declare class BreakerOpenError extends Error { readonly cooldownRemainingMs: number; constructor(name: string, cooldownRemainingMs: number); } /** * Thrown when a call arrives inside an engine's minimum inter-request interval. * The engine is SKIPPED (not waited on) to avoid poisoning pool deadlines and * serializing multi-query fan-out. Subclasses BreakerOpenError so the existing * pool catch maps it to a `skipped: true` outcome with no forwarding change; * `cooldownRemainingMs` carries the time until the engine is dispatchable again. */ export declare class ThrottledError extends BreakerOpenError { constructor(name: string, cooldownRemainingMs: number); } export declare function wrapWithRetryAndBreaker(engine: SearchEngine, cfg?: BreakerConfig): SearchEngine; export declare function runEnginesParallel(entries: EngineEntry[], query: string, options?: SearchEngineOptions, runOptions?: RunEnginesOptions): Promise; //# sourceMappingURL=engine-base.d.ts.map