/**
 * Five retrieval entry points corresponding to the V7 injection triggers
 * (ARCHITECTURE.md §4.3). Each picks the right mix of tiers + knob values:
 *
 *  ┌─────────────────┬──────────────────────────────────────────────────┐
 *  │ Trigger         │ Tiers       │ Size               │ Notes         │
 *  ├─────────────────┼─────────────┼────────────────────┼───────────────┤
 *  │ turn_start      │ 1 + 2 + 3   │ full               │ "before user" │
 *  │ tool_driven     │ 2 (+ 3)     │ shrunk             │ on memory_* call
 *  │ skill_invoke    │ 1 primary   │ shrunk             │ just-in-time  │
 *  │ sub_agent       │ 2 + 3       │ shrunk, no tier1   │ sub-agent ctx │
 *  │ decision_repair │ 1 + 2       │ includeLowValue=ON │ unblock loops │
 *  └─────────────────┴─────────────┴────────────────────┴───────────────┘
 *
 * Each entry is a pure async function: it does storage reads, zero writes.
 * Events (`retrieval.started/.done/.failed`) are emitted via the provided
 * bus so callers can stream packets to the viewer or persist audit trails.
 */
import { ERROR_CODES } from "../../agent-contract/errors.js";
import { ids } from "../id.js";
import { rootLogger } from "../logger/index.js";
import { collectDecisionGuidance } from "./decision-guidance.js";
import { buildQuery } from "./query-builder.js";
import { dedupeTraceEpisodeByEpisodeId } from "./dedupe-trace-episode.js";
import { toPacket, renderSnippetForDebug } from "./injector.js";
import { llmFilterCandidates } from "./llm-filter.js";
import { rank } from "./ranker.js";
import { runTier1 } from "./tier1-skill.js";
import { runTier2Experience } from "./tier2-experience.js";
import { runTier2 } from "./tier2-trace.js";
import { runTier3 } from "./tier3-world.js";
const log = rootLogger.child({ channel: "core.retrieval" });
// ─── Entry point: turn_start ────────────────────────────────────────────────
export async function turnStartRetrieve(deps, ctx, opts = {}) {
    if (deps.config.lightweightMemory) {
        return runAll(deps, ctx, opts, applyPlanOverride({
            wantTier1: false,
            wantTier2: true,
            wantTier3: false,
            includeLowValue: false,
            limit: opts.limit ?? Math.max(1, deps.config.tier2TopK),
            traceOnly: true,
        }, opts.plan));
    }
    return runAll(deps, ctx, opts, applyPlanOverride({
        wantTier1: true,
        wantTier2: true,
        wantTier3: true,
        includeLowValue: deps.config.includeLowValue,
        limit: opts.limit ??
            deps.config.tier1TopK + deps.config.tier2TopK + deps.config.tier3TopK,
    }, opts.plan));
}
// ─── Entry point: tool_driven ───────────────────────────────────────────────
export async function toolDrivenRetrieve(deps, ctx, opts = {}) {
    // Tool-driven retrievals are smaller — we've already spent a turn budget;
    // we only mine Tier 2 (+ optional Tier 3 if vec available). Tier-1 is
    // skipped to avoid re-injecting skills the agent already saw at turn_start.
    return runAll(deps, ctx, opts, {
        wantTier1: false,
        wantTier2: true,
        wantTier3: deps.config.lightweightMemory ? false : true,
        includeLowValue: deps.config.includeLowValue,
        limit: opts.limit ?? Math.max(1, deps.config.tier2TopK),
        traceOnly: deps.config.lightweightMemory,
    });
}
// ─── Entry point: skill_invoke ──────────────────────────────────────────────
export async function skillInvokeRetrieve(deps, ctx, opts = {}) {
    if (deps.config.lightweightMemory) {
        return runAll(deps, ctx, opts, {
            wantTier1: false,
            wantTier2: true,
            wantTier3: false,
            includeLowValue: false,
            limit: opts.limit ?? Math.max(1, deps.config.tier2TopK),
            traceOnly: true,
        });
    }
    // Just-in-time: the agent is about to execute a named Skill. We want
    // (a) the actual Skill's invocation guide if still fresh, and (b) a
    // handful of trace hits to double-check it's the right call.
    return runAll(deps, ctx, opts, {
        wantTier1: true,
        wantTier2: true,
        wantTier3: false,
        includeLowValue: false,
        limit: opts.limit ?? Math.max(1, deps.config.tier1TopK + 2),
    });
}
// ─── Entry point: sub_agent ─────────────────────────────────────────────────
export async function subAgentRetrieve(deps, ctx, opts = {}) {
    return runAll(deps, ctx, opts, {
        wantTier1: false,
        wantTier2: true,
        wantTier3: deps.config.lightweightMemory ? false : true,
        includeLowValue: false,
        limit: opts.limit ?? deps.config.tier2TopK + deps.config.tier3TopK,
        traceOnly: deps.config.lightweightMemory,
    });
}
// ─── Entry point: decision_repair ───────────────────────────────────────────
export async function repairRetrieve(deps, ctx, opts = {}) {
    // Only kicks in after we've hit `failureCount ≥ threshold`. The packet
    // may be `null` when we have no relevant history — callers should treat
    // that as "don't inject anything".
    if (deps.config.lightweightMemory)
        return null;
    if (ctx.failureCount <= 0)
        return null;
    const result = await runAll(deps, ctx, opts, {
        wantTier1: true,
        wantTier2: true,
        wantTier3: false,
        includeLowValue: true, // anti-patterns live at priority=0
        limit: opts.limit ?? deps.config.tier1TopK + deps.config.tier2TopK,
    });
    if (result.stats.emptyPacket)
        return null;
    return result;
}
function applyPlanOverride(plan, override) {
    if (!override)
        return plan;
    return {
        ...plan,
        scenarioId: override.scenarioId ?? plan.scenarioId,
        wantTier1: override.wantTier1 ?? plan.wantTier1,
        wantTier2: override.wantTier2 ?? plan.wantTier2,
        wantTier3: override.wantTier3 ?? plan.wantTier3,
        limit: override.limit ?? plan.limit,
    };
}
async function runAll(deps, ctx, opts, plan) {
    const agent = ctx.agent ?? "openclaw";
    const sessionId = ctx.sessionId;
    const episodeId = ctx.episodeId;
    const ts = deps.now();
    const compiled = buildQuery(ctx);
    opts.events?.emit({
        kind: "retrieval.started",
        reason: ctx.reason,
        agent,
        sessionId,
        episodeId,
        queryTags: compiled.tags,
        ts,
    });
    try {
        const embeddingStats = {
            attempted: compiled.text.length > 0,
            ok: false,
            degraded: false,
        };
        const queryVec = compiled.text
            ? await deps.embedder.embed(compiled.text, "query").then((vec) => {
                embeddingStats.ok = true;
                return vec;
            }).catch((err) => {
                const code = err?.code;
                const message = err instanceof Error ? err.message : String(err);
                embeddingStats.degraded = true;
                embeddingStats.errorCode = code;
                embeddingStats.errorMessage = message;
                log.warn("embed_failed", {
                    reason: ctx.reason,
                    code,
                    err: message,
                });
                return null;
            })
            : null;
        // The keyword channels (FTS + pattern) work even without an embedder,
        // so we no longer short-circuit on `emptyVec`. We only require *some*
        // channel to be armed.
        const haveKeywordChannel = !!compiled.ftsMatch || (compiled.patternTerms?.length ?? 0) > 0;
        const noUsableChannel = !queryVec && !haveKeywordChannel;
        // Kick off the tiers in parallel — each resolves to its own list.
        const wantTier1 = plan.wantTier1 && deps.config.tier1TopK > 0;
        const wantTier2 = plan.wantTier2 && deps.config.tier2TopK > 0;
        const wantTier3 = plan.wantTier3 && deps.config.tier3TopK > 0;
        const traceOnly = plan.traceOnly === true || deps.config.lightweightMemory === true;
        const tier1Start = Date.now();
        const tier1Promise = wantTier1 && !noUsableChannel
            ? runTier1({ repos: deps.repos, config: deps.config }, {
                kind: "embedded",
                queryVec: queryVec ?? null,
                rawText: compiled.text,
                ftsMatch: compiled.ftsMatch,
                patternTerms: compiled.patternTerms,
            })
            : Promise.resolve([]);
        const tier2Start = Date.now();
        const tier2Promise = wantTier2 && !noUsableChannel
            ? runTier2({ repos: deps.repos, config: deps.config, now: deps.now }, {
                queryVec: queryVec ?? null,
                tags: compiled.tags,
                structuralFragments: compiled.structuralFragments,
                ftsMatch: compiled.ftsMatch,
                patternTerms: compiled.patternTerms,
                includeLowValue: plan.includeLowValue,
                excludeSessionId: ctx.reason === "turn_start" && sessionId ? sessionId : undefined,
            })
            : Promise.resolve({ traces: [], episodes: [] });
        const tier2ExperiencePromise = wantTier2 && !traceOnly && !noUsableChannel
            ? runTier2Experience({ repos: deps.repos, config: deps.config }, {
                queryVec,
                ftsMatch: compiled.ftsMatch,
                patternTerms: compiled.patternTerms,
            })
            : Promise.resolve([]);
        const tier3Start = Date.now();
        const tier3Promise = wantTier3 && !noUsableChannel
            ? runTier3({ repos: deps.repos, config: deps.config }, {
                queryVec: queryVec ?? null,
                ftsMatch: compiled.ftsMatch,
                patternTerms: compiled.patternTerms,
            })
            : Promise.resolve([]);
        const [tier1, tier2, tier2Experiences, tier3] = await Promise.all([
            tier1Promise,
            tier2Promise,
            tier2ExperiencePromise,
            tier3Promise,
        ]);
        const tier1LatencyMs = wantTier1 ? Date.now() - tier1Start : 0;
        const tier2LatencyMs = wantTier2 ? Date.now() - tier2Start : 0;
        const tier3LatencyMs = wantTier3 ? Date.now() - tier3Start : 0;
        const fuseStart = Date.now();
        const rawCandidateCount = tier1.length +
            tier2.traces.length +
            tier2.episodes.length +
            tier2Experiences.length +
            tier3.length;
        const ranked = rank({
            tier1,
            tier2Traces: tier2.traces,
            tier2Episodes: traceOnly ? [] : tier2.episodes,
            tier2Experiences,
            tier3,
            limit: plan.limit,
            config: deps.config,
            now: deps.now(),
        });
        const mechanicalRanked = ctx.reason !== "decision_repair" &&
            requiresKeywordConfirmation(compiled.text)
            ? ranked.ranked.filter((candidate) => bypassesKeywordConfirmation(candidate) || hasKeywordChannel(candidate))
            : ranked.ranked;
        const fuseLatencyMs = Date.now() - fuseStart;
        // ─── LLM relevance filter ──────────────────────────────────────────
        // Mechanical retrieval produces high-recall but low-precision
        // candidates. A small LLM round-trip (see `llm-filter.ts`) prunes
        // items that share surface keywords with the query but aren't
        // actually relevant. Full mode fails open to preserve recall;
        // lightweight mode fails closed because it promises summarizer-LLM
        // screened raw memories only.
        const queryText = ctx.userText ?? compiled.text ?? "";
        const filterResult = opts.skipLlmFilter
            ? {
                kept: mechanicalRanked,
                dropped: [],
                outcome: "deferred_to_final",
                sufficient: null,
            }
            : await llmFilterCandidates({ query: queryText, ranked: mechanicalRanked, episodeId }, {
                llm: deps.llm ?? null,
                log,
                config: deps.config,
            });
        const filtered = !opts.skipLlmFilter &&
            deps.config.lightweightMemory &&
            !llmFilterSucceeded(filterResult.outcome)
            ? {
                ...filterResult,
                kept: [],
                dropped: [...filterResult.dropped, ...filterResult.kept],
            }
            : filterResult;
        log.debug("llm_filter.done", {
            outcome: filtered.outcome,
            enforced: deps.config.lightweightMemory && filtered !== filterResult,
            sufficient: filtered.sufficient,
            raw: rawCandidateCount,
            afterThreshold: mechanicalRanked.length,
            droppedByThreshold: ranked.droppedByThreshold,
            thresholdFloor: round(ranked.thresholdFloor, 3),
            topRelevance: round(ranked.topRelevance, 3),
            kept: filtered.kept.length,
            dropped: filtered.dropped.length,
            channels: ranked.channelHits,
        });
        // V7 §2.4.6 — gather preference / anti-pattern from policies that
        // share evidence with what we just retrieved. Cheap (one bounded
        // scan of active policies) and produces nothing when there's
        // nothing to say, so it's safe to call unconditionally here.
        const { ranked: dedupedKept, dedupedByEpisodeCount } = dedupeTraceEpisodeByEpisodeId(filtered.kept);
        const decisionGuidance = traceOnly
            ? undefined
            : collectDecisionGuidance({
                ranked: dedupedKept,
                repos: deps.repos,
            });
        if (decisionGuidance &&
            (decisionGuidance.preference.length > 0 ||
                decisionGuidance.antiPattern.length > 0)) {
            log.debug("decision_guidance.collected", {
                preference: decisionGuidance.preference.length,
                antiPattern: decisionGuidance.antiPattern.length,
                policyIdsTouched: decisionGuidance.policyIdsTouched.length,
            });
        }
        const { packet } = toPacket({
            ranked: dedupedKept,
            reason: ctx.reason,
            tierLatencyMs: {
                tier1: tier1LatencyMs,
                tier2: tier2LatencyMs,
                tier3: tier3LatencyMs,
            },
            now: deps.now(),
            // Fall back to synthetic ids when a retrieval entry point was
            // invoked outside a live turn (CLI preview, tests). The runtime
            // orchestrator overwrites these via `stamped` before the packet
            // reaches the adapter.
            sessionId: sessionId ?? `adhoc-session-${ids.span()}`,
            episodeId: episodeId ?? `adhoc-episode-${ids.span()}`,
            // V7 §2.6 — Tier-1 default = "summary" so we surface skill
            // descriptors + a `memos_skill_get(...)` invocation hint instead of
            // inlining every full guide. Hosts without tool support can flip
            // this to "full" via `algorithm.retrieval.skillInjectionMode`.
            skillInjectionMode: deps.config.skillInjectionMode,
            skillSummaryChars: deps.config.skillSummaryChars,
            decisionGuidance,
        });
        // Surface the dropped-by-LLM candidates so the Logs page can show
        // "initial N → kept M" without the viewer having to re-run the
        // mechanical pipeline.
        packet.droppedByLlm = filtered.dropped
            .map((r) => renderSnippetForDebug(r.candidate))
            .filter((s) => s !== null);
        const stats = {
            reason: ctx.reason,
            scenarioId: plan.scenarioId,
            agent,
            sessionId,
            episodeId,
            plannedTiers: {
                tier1: plan.wantTier1,
                tier2: plan.wantTier2,
                tier3: plan.wantTier3,
            },
            tier1Count: tier1.length,
            tier2Count: tier2.traces.length + (traceOnly ? 0 : tier2.episodes.length) + tier2Experiences.length,
            tier3Count: tier3.length,
            tier1LatencyMs,
            tier2LatencyMs,
            tier3LatencyMs,
            fuseLatencyMs,
            totalLatencyMs: Date.now() - ts,
            queryTokens: approxTokens(compiled.text),
            queryTags: compiled.tags,
            emptyPacket: packet.snippets.length === 0,
            embedding: embeddingStats,
            rawCandidateCount,
            droppedByThresholdCount: ranked.droppedByThreshold,
            thresholdFloor: ranked.thresholdFloor,
            topRelevance: ranked.topRelevance,
            rankedCount: mechanicalRanked.length,
            llmFilterOutcome: filtered.outcome,
            llmFilterSufficient: filtered.sufficient ?? undefined,
            llmFilterKept: filtered.kept.length,
            llmFilterDropped: filtered.dropped.length,
            dedupedByEpisodeCount: dedupedByEpisodeCount > 0 ? dedupedByEpisodeCount : undefined,
            channelHits: ranked.channelHits,
        };
        log.info("done", {
            reason: ctx.reason,
            sessionId,
            tier1: tier1.length,
            tier2: tier2.traces.length,
            tier2Ep: traceOnly ? 0 : tier2.episodes.length,
            tier2Experience: tier2Experiences.length,
            tier3: tier3.length,
            kept: packet.snippets.length,
            totalMs: stats.totalLatencyMs,
        });
        opts.events?.emit({
            kind: "retrieval.done",
            reason: ctx.reason,
            agent,
            sessionId,
            episodeId,
            packet,
            stats,
            ts: deps.now(),
        });
        return { packet, stats };
    }
    catch (err) {
        const code = err?.code ?? ERROR_CODES.INTERNAL;
        const message = err instanceof Error ? err.message : String(err);
        log.error("failed", {
            reason: ctx.reason,
            sessionId,
            err: { code, message },
        });
        opts.events?.emit({
            kind: "retrieval.failed",
            reason: ctx.reason,
            agent,
            sessionId,
            episodeId,
            error: { code, message },
            ts: deps.now(),
        });
        return emptyResult(ctx.reason, agent, sessionId, episodeId, ts, deps.now());
    }
}
function emptyResult(reason, agent, sessionId, episodeId, startedAt, finishedAt) {
    return {
        packet: {
            reason,
            snippets: [],
            rendered: "",
            tierLatencyMs: { tier1: 0, tier2: 0, tier3: 0 },
            packetId: `empty-${startedAt}`,
            ts: finishedAt,
            sessionId,
            episodeId: episodeId ?? `adhoc-episode-${ids.span()}`,
        },
        stats: {
            reason,
            agent,
            sessionId,
            episodeId,
            tier1Count: 0,
            tier2Count: 0,
            tier3Count: 0,
            tier1LatencyMs: 0,
            tier2LatencyMs: 0,
            tier3LatencyMs: 0,
            fuseLatencyMs: 0,
            totalLatencyMs: finishedAt - startedAt,
            queryTokens: 0,
            queryTags: [],
            emptyPacket: true,
            embedding: { attempted: false, ok: false, degraded: false },
        },
    };
}
function requiresKeywordConfirmation(text) {
    const tokens = text.match(/[A-Za-z0-9_:-]{12,}/g) ?? [];
    return tokens.some((token) => {
        const hasIdentifierShape = /[_:-]/.test(token) || /\d/.test(token);
        const hasEnoughEntropy = /[A-Za-z]/.test(token) && token.length >= 16;
        return hasIdentifierShape && hasEnoughEntropy;
    });
}
function hasKeywordChannel(candidate) {
    return (candidate.candidate.channels ?? []).some((channel) => channel.channel === "fts" ||
        channel.channel === "pattern" ||
        channel.channel === "structural");
}
function bypassesKeywordConfirmation(candidate) {
    const refKind = candidate.candidate.refKind;
    return refKind === "skill" || refKind === "world-model";
}
function approxTokens(s) {
    if (!s)
        return 0;
    return Math.ceil(s.length / 4);
}
function round(n, d) {
    if (!Number.isFinite(n))
        return n;
    const f = 10 ** d;
    return Math.round(n * f) / f;
}
function llmFilterSucceeded(outcome) {
    return outcome === "llm_kept_all" || outcome === "llm_filtered";
}
/** Thin façade so pipelines can `new Retriever(deps)` if they prefer OO. */
export class Retriever {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    turnStart(ctx, opts) {
        return turnStartRetrieve(this.deps, ctx, opts);
    }
    toolDriven(ctx, opts) {
        return toolDrivenRetrieve(this.deps, ctx, opts);
    }
    skillInvoke(ctx, opts) {
        return skillInvokeRetrieve(this.deps, ctx, opts);
    }
    subAgent(ctx, opts) {
        return subAgentRetrieve(this.deps, ctx, opts);
    }
    repair(ctx, opts) {
        return repairRetrieve(this.deps, ctx, opts);
    }
}
//# sourceMappingURL=retrieve.js.map