/** * Query funnel diagnosis. * * Turns the engine's `_DONE.json` completion marker into a stage-by-stage * funnel plus a single verdict the MCP — and the agent reading its output — * can branch on to debug a zero-result query, instead of seeing a bare * "0 events" and guessing. * * The engine's query path is a 5-stage assembly line: * * dispatch -> resolve blobs -> bloom scan -> filter match -> stream events * * The coordinator's `_DONE` marker carries counters for the stages it can see. * One structural caveat: in REMOTE dispatch the coordinator hands each * time-slice to a subquery and does no scanning itself, so its own * `scanned`/`matched` counters stay 0 and do NOT reflect what the subqueries * found. A `reason:"dispatched"` outcome with zero downstream counts is * therefore genuinely ambiguous — we report DISPATCHED_BLIND and say exactly * why, rather than pretend to localize it. Every other reason the marker * reports precisely, and `eventsReturned` (the actual qr/ files on disk) is * always authoritative for success. * * `reason` values come from the engine close() classifier (IndexQueryWriter): * dispatched | empty-range | bloom-miss | match-no-dispatch | success | unknown */ /** The `_DONE.json` marker the engine coordinator writes to qr//. */ export interface DoneMarker { queryId?: string; /** Engine close() classifier; the primary signal. */ reason?: string; /** Blobs the COORDINATOR scanned (0 in remote dispatch — see file header). */ scanned?: number; /** Blobs that passed the bloom (coordinator view). */ matched?: number; skippedSearch?: number; skippedTemplate?: number; streamRequests?: number; streamBlobs?: number; /** Subqueries fanned out (remote dispatch). */ submittedTasks?: number; expectedMarkers?: number; elapsedMs?: number; } export type QueryVerdict = 'OK' | 'NO_MARKER' | 'EMPTY_RANGE' | 'BLOOM_REJECTED_ALL' | 'MATCHED_NO_EVENTS' | 'FILTER_NO_MATCH' | 'FETCH_EMPTY' | 'PARSE_EMPTY' | 'FETCH_OR_PARSE_EMPTY' | 'DELIVERY_INCOMPLETE' | 'DISPATCHED_BLIND' | 'INCONCLUSIVE'; export interface QueryFunnel { /** Subqueries fanned out (submittedTasks). null when not reported. */ dispatched: number | null; /** * Blobs the RESOLUTION stage mapped for the window (sum of `scan range:` * submittedKeys). Ground-truth only; only set by diagnoseFromStats. 0 => the * time->blob mapping returned nothing. */ resolvedBlobs?: number | null; /** Blobs scanned. NOTE: coordinator-only in remote dispatch (see coordinatorBlind). */ scanned: number | null; /** Blobs that passed the bloom. Coordinator-only in remote dispatch. */ bloomMatched: number | null; skippedSearch: number | null; skippedTemplate: number | null; streamRequests: number | null; /** * Volume (utf8 bytes) of events FETCHED from the matched byte-ranges, pre-filter * (q/ writer "fetched N bytes"). Ground-truth only. >0 with 0 written => * FILTER_NO_MATCH (fetched but predicate wrote none); 0 => FETCH_OR_PARSE_EMPTY. * Despite the engine's "fetched" label this is event volume, not S3-GET bytes. */ fetchedVolume?: number | null; /** Bytes read from the source object (S3 GET). The real fetch counter; splits FETCH_EMPTY from PARSE_EMPTY. */ s3BytesRead?: number | null; /** * Events that reached the results writer but were dropped by the exact * predicate pre-write (results-writer emptyFlushes). Ground-truth only; a * corroborator for FILTER_NO_MATCH alongside fetchedVolume. */ filterDropped?: number | null; /** Events actually downloaded from qr/ — always authoritative. */ eventsReturned: number; /** True => scanned/bloomMatched reflect only the coordinator, not the subqueries. */ coordinatorBlind: boolean; } export interface QueryDiagnosis { verdict: QueryVerdict; funnel: QueryFunnel; /** Plain-English statement of what the funnel shows. */ explanation: string; /** What to do next — the actionable branch for the agent. */ hint: string; } /** * Per-stage stats parsed from the engine's own DEBUG/PERF CloudWatch events * (retriever-diagnostics.buildDiagnostics) — the GROUND TRUTH for what each * stage actually did, as opposed to the coordinator marker's blind aggregate. * Every field is what the engine reported; nothing is inferred. */ export interface CloudWatchStageStats { /** * Index blobs the RESOLUTION stage mapped for the window (sum of `scan range:` * submittedKeys). 0 => the time->blob mapping returned nothing (empty window). * The ground-truth empty signal, distinct from scanned/bloom. */ submittedKeys?: number; /** Index blobs the bloom scan examined. */ scanned?: number; /** Blobs whose bloom matched the search (candidates to fetch). */ matched?: number; /** Stream workers dispatched to fetch matched objects. */ streamWorkers?: number; /** Stream workers that reported completion. */ workersComplete?: number; /** * Written-event utf8 volume from the q/ writer ("stream worker complete: * fetched N bytes"). NOT the S3-read byte count — informational only; never * used as a fetch success/failure signal (the engine emits no S3-read counter). */ fetchedBytes?: number; /** Events the results writer wrote after the exact predicate (qr/). */ resultEvents?: number; /** * Flushes that reached the results writer but were empty because the exact * predicate dropped the event pre-write. The distinguisher for a 0-result: * >0 (with 0 written) = the filter rejected events that arrived; 0 (with 0 * written) = no rows reached the writer (the fetch or the parse produced none). */ emptyFlushes?: number; /** Events dropped at the results cap. */ resultsTruncated?: number; /** * Bytes actually read from the source object (S3 GET), from the engine's * "stream worker fetch complete: s3BytesRead" PERF event. The REAL fetch * counter (unlike fetchedBytes, which is pre-filter event volume). When * present it splits a 0-result cleanly: ==0 => FETCH_EMPTY (read returned * nothing); >0 with 0 events parsed => PARSE_EMPTY. Absent on engines that * predate the counter (the funnel falls back to FETCH_OR_PARSE_EMPTY). */ s3BytesRead?: number; /** * The exact in-memory predicate applied to fetched events (queryFilters), from * the "query plan" event's filter= field. Surfaced in a FILTER_NO_MATCH * explanation so the agent sees what was matched, not just that nothing passed. */ filterExpr?: string; } /** * Ground-truth verdict from the engine's per-stage CloudWatch events. Returns * null when the stats are absent/empty (CW not configured, or the run wasn't * DEBUG) so the caller falls back to the marker-only `diagnoseQuery`. * * The funnel, stage by stage, exactly as the engine reports it: * resolved(submittedKeys) -> scanned -> matched(bloom) -> workers -> * resultEvents written / emptyFlushes dropped -> delivered * * The verdict turns on RESOLUTION (submittedKeys===0 => EMPTY_RANGE) and the * fetch/filter split (matched>0, written===0: fetchedBytes>0 OR emptyFlushes>0 => * FILTER_NO_MATCH, i.e. events were fetched+parsed but the exact predicate wrote * none; both 0 => FETCH_OR_PARSE_EMPTY). The q/ "fetched bytes" is event volume * pre-filter, NOT S3-GET bytes — so >0 proves fetch+parse worked, but it can't * separate a real S3-read failure from a parse miss (that needs an engine counter). */ export declare function diagnoseFromStats(s: CloudWatchStageStats | null | undefined, eventsReturned: number): QueryDiagnosis | null; /** * Diagnose a query from its `_DONE` marker and the number of events that * actually came back. Pure + side-effect-free so it is trivially testable. */ export declare function diagnoseQuery(done: DoneMarker | null, eventsReturned: number, opts?: { failedWorkerFiles?: number; }): QueryDiagnosis; /** Compact one-line funnel for embedding in a query envelope / log. */ export declare function formatFunnel(d: QueryDiagnosis): string;