/**
 * Memoization cache for primitive outputs (DURABLE.3).
 *
 * Primitives that declare `memoizable: true` (DURABLE.2) cache outputs keyed
 * by `(fn, inputs_hash)`. Identical inputs → cached output, primitive not
 * invoked. The evaluator wraps the cache check around `invokeDurable`'s
 * checkpoint-restore branch: a checkpoint hit (same run, same step) still
 * wins; a memo hit serves cross-run replays of identical-input calls.
 *
 * Two-tier design:
 *
 *   Memory tier (`LRUCache<string, MemoryEntry>`): hot, ~1000 entries cap,
 *   per-entry TTL via the entry's `expiresAtMs`. Sub-millisecond hit; rebuilt
 *   on demand from the persistent tier after a daemon restart.
 *
 *   libsql tier (`memo_cache` table): persistent across daemon restarts.
 *   `idx_memo_cached_at` indexes age for the prune verb. Outputs round-trip
 *   through `canonical_json` so Date/Buffer survive (same envelope rules as
 *   DURABLE.1's checkpoint store).
 *
 * Singleflight (in-flight Promise dedup):
 *
 *   A cache stampede — 100 concurrent calls for the same miss → 100 primitive
 *   invocations — defeats the whole point. `get()` keeps an `inflight` map
 *   keyed by the same `${fn}::${inputsHash}` string. When N callers race on a
 *   miss, the FIRST caller's `set()` writes the entry and clears the inflight
 *   record; the remaining N-1 callers await the same Promise and read from
 *   the cache once it settles. The runtime caller pattern is:
 *
 *     const cached = await cache.get(fn, hash, ttl);
 *     if (cached !== null) return cached.value;
 *     return cache.singleflight(fn, hash, async () => {
 *       const fresh = await invokePrimitive();
 *       await cache.set(fn, hash, fresh, ttl);
 *       return fresh;
 *     });
 *
 *   That keeps singleflight orthogonal to the cache lookup: a clean caller
 *   can opt out (e.g. tests verifying miss-path semantics) by skipping
 *   `singleflight()` and calling the primitive directly.
 *
 * TTL policy:
 *
 *   Caller provides a `ttlMs?: number` to both `get()` and `set()`. When
 *   omitted, the entry has NO TTL (lives until evicted by LRU or cleared by
 *   `clear({fn,olderThanMs})`). The runtime wrap (evaluator) uses the
 *   per-primitive class defaults below; the cache itself is policy-free.
 *
 *     LLM (llm_classify):        3_600_000  ( 1h)
 *     RAG (recall, embed):         300_000  ( 5m)
 *     http_request:                 30_000  (30s)
 *     check_destination:          3_600_000  ( 1h)
 *
 *   These match the spec's risk-callout for stale outputs in state-sensitive
 *   primitives and are tuned to be conservative (RAG indexes change often;
 *   HTTP responses are the most volatile; LLM classifications on the same
 *   prompt+model+temperature are stable enough for an hour).
 *
 * Privacy / PII:
 *
 *   Cached prompts MAY contain PII — `llm_classify` args carry whatever the
 *   pack passes in, which can include user emails, file contents, names.
 *   Two mitigations:
 *
 *     1. The persistent tier stores `outputs_json` AND `inputs_hash` (the
 *        hash of inputs, not the raw inputs themselves). Inputs are never
 *        persisted in this module. The hash is sha256, non-reversible.
 *
 *     2. Pack authors who handle sensitive payloads should set
 *        `cached_at_ms TTL ≤ pack sensitivity threshold` — the cache
 *        enforces TTL, the pack chooses the threshold. The pack-declared
 *        sensitivity threshold work lives in a later track (privacy.md is
 *        not in scope for DURABLE.3); until then, callers pass conservative
 *        TTLs and rely on `clear()` for explicit purges.
 *
 * Imports from: @libsql/client, lru-cache, ./canonical_json.js.
 * Imported by: evaluator (DURABLE.3 wrap), future `opensquid cache` CLI.
 */
import type { Client } from '@libsql/client';
export interface MemoStats {
    fn: string;
    hits: number;
    size: number;
}
export interface MemoCacheOpts {
    /** Max entries in the memory tier. Default 1000. */
    memoryMax?: number;
    /** Injected clock; defaults to `Date.now`. */
    nowMs?: () => number;
}
export declare class MemoCache {
    private readonly db;
    private readonly memory;
    private readonly inflight;
    private readonly nowMs;
    private initialized;
    constructor(db: Client, opts?: MemoCacheOpts);
    /**
     * Idempotent DDL. Safe to call repeatedly; only the first call hits
     * libsql. Same posture as `CheckpointStore.init` and `RateLimiter.init`.
     */
    init(): Promise<void>;
    /**
     * Two-tier lookup.
     *
     *   1. Memory hit + not expired → return value.
     *   2. Memory miss → libsql lookup.
     *   3. Libsql hit + not expired → bump hit_count, restore to memory tier,
     *      return value.
     *   4. Otherwise → null (miss).
     *
     * `ttlMs` is the CALLER's TTL contract. When the persistent row has its
     * OWN `ttl_ms` set, that value is authoritative — the row's TTL was the
     * one in effect at `set()` time, and re-interpreting it under a different
     * TTL at read time would silently extend or shrink the entry's lifetime.
     * The `ttlMs` parameter on `get()` is only used when the row has a NULL
     * `ttl_ms` AND the caller wants to retroactively bound an indefinite
     * entry (rare; supplied for symmetry with `set()`).
     *
     * Returns the cached value (which may be `null`-as-cached-output — the
     * spec stores the primitive's return value verbatim, so a primitive that
     * legitimately returned `null` would store `null`; we wrap the hit/miss
     * signal in the return type's nullability convention by distinguishing
     * `null` (miss) from a wrapped value (hit).
     *
     * Convention: callers should check `=== null` for miss. A primitive that
     * really returned `null` ends up encoded as the JSON `null` string in the
     * persistent tier, and on read we return a wrapper to disambiguate — see
     * the `MemoHit` type below.
     */
    get(fn: string, inputsHash: string, ttlMs?: number): Promise<MemoHit | null>;
    /**
     * Write both tiers. Memory first (cheap, in-process), then libsql (the
     * source of truth across restarts). `INSERT OR REPLACE` keeps the write
     * idempotent: the same `(fn, inputs_hash)` written twice in a row
     * preserves the latest output and resets `cached_at_ms` to the new write
     * time. `hit_count` is reset to 0 because the entry is logically new.
     *
     * When `ttlMs` is omitted (or explicitly `undefined`), the persistent row
     * stores `NULL` — no expiry; the entry lives until evicted by LRU
     * pressure or removed by `clear()`.
     */
    set(fn: string, inputsHash: string, outputs: unknown, ttlMs?: number): Promise<void>;
    /**
     * Singleflight: dedup concurrent misses on the same key. The first caller
     * runs `compute()`; subsequent callers await the same Promise. The
     * inflight record is removed in `finally` so a failure on the first
     * caller doesn't poison the key forever.
     *
     * Important: this method does NOT itself cache the result — `compute()`
     * is expected to call `set()` after the primitive returns. The wrap
     * pattern is documented in the file header.
     *
     * Returning `unknown` keeps the API substrate-shaped: the runtime caller
     * casts to the primitive's return type once the FunctionRegistry has
     * confirmed args + output handling.
     */
    singleflight<T>(fn: string, inputsHash: string, compute: () => Promise<T>): Promise<T>;
    /**
     * Invalidate cache entries. Both tiers honored.
     *
     *   - `{}`                       → wipe everything.
     *   - `{ fn: 'llm_classify' }`   → wipe all llm_classify entries.
     *   - `{ olderThanMs: 7 * 86_400_000 }` → wipe entries cached more than
     *      7 days ago.
     *   - `{ fn, olderThanMs }`      → AND of both predicates.
     *
     * Returns the number of persistent-tier rows removed. The memory tier's
     * affected-count is not surfaced (the LRU might have already evicted the
     * row, in which case "removed" is undefined; callers care about durable
     * cleanup).
     */
    clear(opts?: {
        fn?: string;
        olderThanMs?: number;
    }): Promise<number>;
    /**
     * Per-primitive stats: hit_count totals + live row counts. Powers the
     * future `opensquid cache stats` CLI verb.
     *
     * Expired rows are excluded from `size` — the count reflects rows that
     * would actually serve traffic today. The persistent table is NOT
     * compacted as a side effect of this call; the GC verb is `clear()` with
     * an `olderThanMs` argument.
     */
    stats(): Promise<MemoStats[]>;
}
/**
 * Hit envelope. Distinguishes a real cached `null` (a primitive that
 * returned null and stored it) from a miss (`get` returns `null` itself).
 */
export interface MemoHit {
    value: unknown;
}
//# sourceMappingURL=memo_cache.d.ts.map