/** * LC1 — retrieval-trace persistence * (docs/plans/2026-08-02-lc1-recall-trace-persistence.md). * * Single producer for the `recall_traces` / `recall_trace_results` / * `recall_trace_outcomes` tables (schema v40). Every recall on the three * wired paths (api.recall, api.getContext, CLI cmdRecall) writes a trace * row: the ids + ranks + scores actually returned. Outcome events that * resolve their targets from last-retrieval state link back to the trace * they judge via `recordTraceOutcome`. This is the (query, shown, outcome) * training triple every Track LC learned component needs. * * All writes here are fail-soft: a broken trace write must never break the * surrounding recall or outcome call. Failures are logged to stderr and * swallowed (matches the api.ts ~2843 "audit emit failed" precedent — no * new debug env var). */ import { type DatabaseSyncLike } from './db.js'; import type { RerankStep } from './search.js'; /** One ranked result to persist alongside its trace row. */ export interface RecallTraceResultInput { memoryId: string; score: number; /** Per-stage rerank steps when the caller ran with explain/--why; omitted * (or empty) persists `rerank_json` as NULL. */ rerankSteps?: RerankStep[]; } /** Input to `writeRecallTrace` / `writeRecallTraceAtRoot`. */ export interface RecallTraceInput { tenantId: string; sessionId?: string | null; pipeline: 'api' | 'cli' | 'context' | 'mcp'; /** Raw query text. NEVER persisted — only its sha256/16 hash + length are * stored (GDPR Path A / audit convention, cli.ts:1532). */ query: string; /** True when the caller ran with explain/--why (per-result rerank steps * may be present). Defaults to false. */ explainMode?: boolean; /** Results in returned rank order (index 0 = rank 1). */ results: RecallTraceResultInput[]; } /** * Insert a `recall_traces` row + its `recall_trace_results` rows in ONE * transaction, on the connection handed in. Fail-soft: never throws — * logs to stderr and returns null on any failure. * * Connection policy (per the plan): api.recall calls this directly on its * own already-open handle. api.getContext and CLI cmdRecall go through * `writeRecallTraceAtRoot` instead, since their audit handles are already * closed by the time tracing runs. */ export declare function writeRecallTrace(db: DatabaseSyncLike, input: RecallTraceInput): number | null; /** * Convenience wrapper: opens a fresh short-lived connection at `root`, * writes the trace, and closes. Returns the new trace id, or null on any * failure (fail-soft). * * Used at api.getContext and CLI cmdRecall — sites where the block's own * convention is per-call handles (writeEntry, saveIndex) and the earlier * audit handles are already closed. NOT used by api.recall, which must * reuse the caller's open handle (v1.11.5 no-side-effects contract, * tests/api-recall-no-side-effects.test.ts). * * F1 structural fix (replaces the earlier stamp-then-clear design): this * function does NOT touch the `last_trace_id` meta key. Stamping lived here * originally, on its own connection, separate from the `last_retrieval_ids` * write in `saveIndex` — two connections meant two commits, so a crash or * a failed second write could advance one without the other. LOCKSTEP * INVARIANT: `last_trace_id` must only ever advance in the SAME write as * `last_retrieval_ids`. The caller now does: call this function FIRST, set * `localIndex.last_trace_id` from the returned id, THEN call `saveIndex` * once — `saveIndex` persists both meta keys in one transaction * (store.ts). Call sites that trace WITHOUT advancing `last_retrieval_ids` * (CLI cmdRecall's zero-result path, getContext's empty-result path) simply * never touch `localIndex` at all — they can't desync by construction. * * Fail-soft: never throws, including on connection failure. */ export declare function writeRecallTraceAtRoot(root: string, input: RecallTraceInput): number | null; /** Input to `recordTraceOutcome`. */ export interface RecordTraceOutcomeInput { traceId: number; tenantId: string; outcome: 'positive' | 'negative'; /** Ids actually credited by this outcome event (post tenant-filtering). */ memoryIds: string[]; } /** * Record an outcome event against a trace, linking the (query, shown, * outcome) triple. Called ONLY where the credited ids actually come from * the last-retrieval mechanism (api.outcomeForLastRecall and any outcome * flow that resolves its targets from last-retrieval state) or from an * SDK caller's explicit `traceId` opt — never unconditionally from * api.outcome, which would mislink an explicit-id caller to a stale, * unrelated trace. * * Lives in its own append-only table, not audit_log metadata: audit_log is * pruned by `pruneAuditLog`, and pruning must never erase training data. * * F4 validation (codex cross-model finding): `traceId`/`memoryIds` reach * this function from caller-side state (`last_trace_id` / applied outcome * ids) that can go stale relative to the trace it names — a forgotten * memory, a tenant switch mid-session, or a race between two callers. Two * checks run before the insert, both skip silently (console.error one * line) rather than throw: * 1. The named trace must exist and belong to `input.tenantId` — a * tenant mismatch or a dangling id (deleted trace) skips. * 2. `input.memoryIds` is intersected against the trace's OWN * `recall_trace_results.memory_id` set — only ids that trace actually * returned are recorded. An id that was never in this trace's result * set (stale caller state) is silently dropped rather than recorded * as a false credit. If the intersection is empty, no row is written. * * Fail-soft: never throws. */ export declare function recordTraceOutcome(db: DatabaseSyncLike, input: RecordTraceOutcomeInput): void; //# sourceMappingURL=recall-trace.d.ts.map