/**
 * Storage layer for Hippo.
 *
 * SQLite is the source of truth.
 * Markdown + JSON files remain as human-readable compatibility mirrors.
 */
import { MemoryEntry, Layer } from './memory.js';
import { getHippoDbPath, type DatabaseSyncLike } from './db.js';
import { SessionHandoff } from './handoff.js';
export interface IndexEntry {
    id: string;
    file: string;
    layer: Layer;
    strength: number;
    tags: string[];
    created: string;
    last_retrieved: string;
    pinned: boolean;
}
export interface HippoIndex {
    version: number;
    entries: Record<string, IndexEntry>;
    last_retrieval_ids: string[];
}
export interface TaskSnapshot {
    id: number;
    task: string;
    summary: string;
    next_step: string;
    status: string;
    source: string;
    session_id: string | null;
    scope: string | null;
    created_at: string;
    updated_at: string;
}
export interface MemoryConflict {
    id: number;
    memory_a_id: string;
    memory_b_id: string;
    reason: string;
    score: number;
    status: string;
    detected_at: string;
    updated_at: string;
}
export interface SessionEvent {
    id: number;
    session_id: string;
    task: string | null;
    event_type: string;
    content: string;
    source: string;
    scope: string | null;
    metadata: Record<string, unknown>;
    created_at: string;
}
/**
 * Default candidate-pool size for `loadSearchEntries` when called with
 * `limit === undefined`. Single source of truth; `api.recall` imports
 * this for `RecallResult.windowSize` reporting so the two cannot drift.
 */
export declare const DEFAULT_SEARCH_CANDIDATE_LIMIT = 200;
/**
 * v1.7.2 — literal scopes excluded from recall by default-deny when the
 * caller passes no `scope`. The SQL clause in `loadSearchRows` and the JS
 * helper `passesScopeFilterForRecall` (src/api.ts) both read from this
 * constant. Adding a deny scope is a one-place change.
 *
 * Regex-based denies (e.g. `<source>:private:*`) stay in
 * `passesScopeFilterForRecall` as a separate JS step — they don't translate
 * cleanly to SQL.
 *
 * Invariant: never empty. An empty array would silently allow quarantine
 * scopes through both paths (SQL clause omitted, JS check vacuous). The
 * module-load assertion below pins this loudly.
 */
export declare const RECALL_DEFAULT_DENY_SCOPES: readonly ["unknown:legacy"];
/**
 * @internal v1.7.3 — runtime guard against a future maintainer blanking a
 * load-bearing literal array. Extracted from the inline guard so the throw
 * path is directly testable. `as const` arrays widen via `readonly T[]` at
 * the call site so the empty case is reachable at runtime.
 */
export declare function assertNonEmpty<T>(arr: readonly T[], name: string): void;
export declare function getHippoRoot(cwd?: string): string;
export declare function isInitialized(hippoRoot: string): boolean;
export declare function initStore(hippoRoot: string): void;
/**
 * Serialize a MemoryEntry to markdown with YAML frontmatter.
 */
export declare function serializeEntry(entry: MemoryEntry): string;
/**
 * Deserialize a markdown file to a MemoryEntry.
 */
export declare function deserializeEntry(raw: string): MemoryEntry | null;
/**
 * v1.7.2 — recall-mode scope filter shape, exported so callers
 * (`loadRecallSearchEntries`) and tests can refer to it symbolically without
 * `Parameters<typeof loadSearchRows>[N]` indirection.
 *
 * Two modes:
 *   - 'default-deny' — exclude scopes in `RECALL_DEFAULT_DENY_SCOPES` (T2).
 *   - 'exact' — exact match on `m.scope = value`.
 *
 * Background pipelines (`consolidate`, `embeddings`, `refine-llm`, ...) call
 * `loadSearchEntries` (no scopeFilter arg) and see all rows including
 * quarantine.
 */
/** @internal v1.7.2 — internal SQL-builder shape; not on the public API
 *  surface (not re-exported from `src/index.ts`). Subject to change. */
export type RecallScopeFilter = {
    mode: 'default-deny';
} | {
    mode: 'exact';
    value: string;
};
export declare function removeEntryMirrors(hippoRoot: string, id: string): void;
/**
 * Load the current derived index from SQLite and refresh the mirror file.
 */
export declare function loadIndex(hippoRoot: string): HippoIndex;
/**
 * Persist mutable index metadata. Entry rows themselves are derived from SQLite.
 */
export declare function saveIndex(hippoRoot: string, index: HippoIndex): void;
/**
 * Write a memory entry to SQLite and refresh compatibility mirrors.
 *
 * `opts.actor` defaults to 'cli' so unauthenticated direct-CLI callers still
 * get the right audit attribution. The HTTP server (A1) and api.* layer pass
 * the resolved actor (`api_key:<key_id>` / `localhost:cli`) so audit events
 * land with one row per write, no double-emit.
 *
 * `opts.afterWrite` is invoked inside the same SAVEPOINT as the memories
 * INSERT (mirrors archiveRawMemory's shape in raw-archive.ts). On callback
 * throw, the SAVEPOINT rolls back — the memory row never lands, and the
 * filesystem mirrors / audit emit never run. Used by E1.3+ connectors to
 * stamp idempotency rows atomically with the memory write.
 */
export declare function writeEntry(hippoRoot: string, entry: MemoryEntry, opts?: {
    actor?: string;
    afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
}): void;
/**
 * DB-only write path. Caller owns the open `db` handle. Runs SAVEPOINT +
 * upsert + afterWrite hook + audit row inside the SAVEPOINT scope. Caller
 * is responsible for opening `db`, optionally wrapping in a larger BEGIN/
 * COMMIT (e.g. supersede's BEGIN IMMEDIATE), closing `db`, AND calling
 * `writeEntryMirrors` after the larger tx commits — mirrors must run
 * post-commit so a rolled-back tx never leaves orphan markdown.
 *
 * Audit-order note: the audit row is emitted INSIDE the SAVEPOINT, so audit
 * commits atomically with the row INSERT. A subsequent mirror failure cannot
 * leave a recorded audit entry without its corresponding DB row. This is a
 * documented hardening over the prior writeEntry-as-monolith ordering.
 */
export declare function writeEntryDbOnly(db: DatabaseSyncLike, entry: MemoryEntry, opts?: {
    actor?: string;
    afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
}): void;
/**
 * Filesystem mirrors path. Caller passes `hippoRoot` + an open `db` handle
 * (used by `buildIndexFromDb` to derive the index from the source of truth).
 * MUST be invoked AFTER the outer transaction commits — a mirror write
 * during a tx that subsequently rolls back would leave orphan markdown.
 */
export declare function writeEntryMirrors(hippoRoot: string, db: DatabaseSyncLike, entry: MemoryEntry): void;
/**
 * Read a memory entry by ID.
 *
 * When `tenantId` is provided, the read is scoped to that tenant (cross-tenant
 * lookups return null). When omitted, no tenant filter is applied — preserves
 * legacy single-tenant callers and the writeEntry/readEntry round-trip.
 */
export declare function readEntry(hippoRoot: string, id: string, tenantId?: string): MemoryEntry | null;
/**
 * Batched lookup. Caps at 500 ids per call to keep the IN(?,?,...) clause
 * within SQLite limits. Tenant filter is enforced when `tenantId` is passed.
 * Used by DAG-aware recall (docs/plans/2026-05-05-dag-recall.md Task 1.5)
 * to fetch parent summaries for a set of overflowed leaves.
 */
export declare function loadEntriesByIds(hippoRoot: string, ids: readonly string[], tenantId?: string): MemoryEntry[];
/**
 * All `kind='raw'` rows for a given session, tenant-scoped, returned
 * oldest-first. Used by `api.assemble` to walk a session's chronological
 * context. Excludes superseded rows.
 *
 * Cap semantics (v1.6.2 codex fix): when `cap` is provided, the NEWEST
 * `cap` rows are loaded — `ORDER BY created DESC LIMIT cap` server-side,
 * reversed to oldest-first client-side. Pre-v1.6.2 ordered ASC + LIMIT,
 * which silently dropped the newest rows and broke fresh-tail in assemble.
 *
 * Returns `[]` for an empty sessionId. Final order: `created ASC, id ASC`.
 */
export declare function loadSessionRawMemories(hippoRoot: string, sessionId: string, tenantId?: string, cap?: number): MemoryEntry[];
/**
 * Pre-cap, scope-aware row count for a session. Lets `assemble` report
 * the full session size even when `rowCap` truncates the loaded window,
 * WITHOUT leaking rows the caller wouldn't have been allowed to load.
 *
 * v1.6.3 codex P1 / senior P0: an earlier draft of this helper ran an
 * unscoped COUNT, which let a no-scope caller infer the existence of
 * private rows by comparing `totalRaw` against `items.length`. This
 * version SQL-encodes the same default-deny rule `passesScopeFilterForRecall`
 * applies in TS:
 *   - explicit scope passed: exact-match
 *   - no scope: rows where scope IS NULL, or scope is NOT a `<source>:private:*`
 *     pattern AND not the `unknown:legacy` quarantine bucket.
 *
 * `tenantId` is optional for back-compat. Pass `undefined` only when
 * intentionally counting cross-tenant; `assemble()` passes `ctx.tenantId`.
 */
export declare function countSessionRawMemories(hippoRoot: string, sessionId: string, tenantId?: string, scope?: string): number;
/**
 * Last N kind='raw' memories by `created` desc. Tenant scoped. When
 * `sessionId` is supplied, also constrains to a specific session — that
 * is the correct shape for "what did I just see in THIS session."
 *
 * v1.6.2 codex review fix: pre-v1.6.2 was tenant-wide only. With multiple
 * concurrent sessions in a tenant, fresh-tail recall surfaced unrelated
 * rows from other sessions and stamped them `isFreshTail=true`. Callers
 * that want session-scoped fresh-tail now pass `sessionId`. The
 * tenant-wide form (no sessionId) still exists for "anything new across
 * the whole tenant" — pass undefined to opt in.
 *
 * Bounded count cap at 200 — beyond that the caller should filter via
 * tags/scope rather than time-windowed recall.
 *
 * Deprecation note (v1.6.5) — the **tenant-wide call shape** (omitting
 * `sessionId`) is rarely the right shape for "what did I just see in this
 * conversation". `api.recall` enforces session scoping when
 * `HIPPO_REQUIRE_SESSION_SCOPED_FRESH_TAIL=1` is set, throwing
 * `RecallContractError` instead. Tenant-wide remains the back-compat default
 * but is discouraged for new callers. Passing `sessionId` is fully supported
 * and recommended; this function is NOT deprecated as a whole.
 */
export declare function loadFreshRawMemories(hippoRoot: string, count: number, tenantId?: string, sessionId?: string): MemoryEntry[];
/**
 * Direct DAG children of a parent summary. Tenant scoped. Returns only rows
 * whose `dag_parent_id` matches `parentId`; does NOT walk recursively.
 * Used by `drillDown` (Task 3).
 */
export declare function loadChildrenOf(hippoRoot: string, parentId: string, tenantId?: string): MemoryEntry[];
/**
 * Delete an entry from SQLite and mirrors.
 *
 * `opts.actor` defaults to 'cli'. The api.* layer threads `ctx.actor` so HTTP
 * callers land with `api_key:<key_id>` in the audit log without a duplicate
 * emit from the api wrapper.
 */
export declare function deleteEntry(hippoRoot: string, id: string, opts?: {
    actor?: string;
}): boolean;
/**
 * Batch-write and batch-delete entries in a single transaction.
 * Used by consolidation to avoid N open/close cycles.
 */
export declare function batchWriteAndDelete(hippoRoot: string, toWrite: MemoryEntry[], toDeleteIds: string[]): void;
/**
 * Load all entries from SQLite.
 *
 * When `tenantId` is provided, results are scoped to that tenant. Omitting it
 * yields all rows (legacy behavior used by consolidate/autolearn etc.). Recall
 * paths that surface results to a user MUST pass a resolved tenant.
 */
export declare function loadAllEntries(hippoRoot: string, tenantId?: string): MemoryEntry[];
/**
 * Load likely search candidates directly from SQLite.
 * Uses FTS5 when available, falls back to LIKE matching, then full-store fallback.
 *
 * When `tenantId` is provided, every SELECT (FTS join, LIKE, fallback) filters
 * by tenant_id. Cross-tenant memories never surface. Omitted = no filter.
 */
export declare function loadSearchEntries(hippoRoot: string, query: string, limit?: number, tenantId?: string): MemoryEntry[];
/**
 * v1.7.1 — recall-mode loader. Pushes the recall-side scope predicate into
 * SQL so `unknown:legacy` cannot leak via any consumer that hasn't remembered
 * to re-filter (root-cause-over-patches: codex flagged this on v1.6.5 review).
 *
 * - `requestedScope` undefined / '': default-deny on `unknown:legacy`.
 * - `requestedScope` non-empty string: exact match on `m.scope = requestedScope`.
 *
 * Private-scope (`<source>:private:*`) regex filter remains a JS post-load
 * step in `api.recall()` — the regex doesn't translate cleanly to SQL and the
 * existing `passesScopeFilterForRecall` helper covers it consistently.
 *
 * Consumers: `api.recall` (v1.7.1+). Background pipelines (`consolidate`,
 * `embeddings`, `refine-llm`, ...) keep using `loadSearchEntries` so they
 * can see quarantined rows when needed.
 */
export declare function loadRecallSearchEntries(hippoRoot: string, query: string, limit: number | undefined, tenantId: string, requestedScope?: string): MemoryEntry[];
/**
 * Rebuild mirrors from SQLite, importing any legacy markdown files not already present.
 */
export declare function rebuildIndex(hippoRoot: string): HippoIndex;
export declare function updateStats(hippoRoot: string, delta: {
    remembered?: number;
    recalled?: number;
    forgotten?: number;
}): void;
export declare function loadStats(hippoRoot: string): Record<string, unknown>;
export declare function appendConsolidationRun(hippoRoot: string, run: {
    timestamp: string;
    decayed: number;
    merged: number;
    removed: number;
}): void;
/**
 * Session decay context: provides the data needed for session-based and adaptive decay.
 */
export interface SessionDecayContext {
    /** Total number of sleep (consolidation) cycles completed. */
    sleepCount: number;
    /** Average interval between recent sleep cycles, in days. 0 if < 2 cycles. */
    avgSessionIntervalDays: number;
}
/**
 * Load the session decay context from the store.
 * Uses consolidation_runs timestamps to compute session intervals.
 */
export declare function loadSessionDecayContext(hippoRoot: string): SessionDecayContext;
/**
 * Increment the sleep counter. Called after each consolidation run.
 */
export declare function incrementSleepCount(hippoRoot: string): void;
/**
 * Defensive runtime guard for tenant id arguments.
 *
 * The continuity helpers (saveActiveTaskSnapshot, listSessionEvents, etc.)
 * gained a required `tenantId` parameter in v0.41 / schema v22 to close a
 * cross-tenant data leak. TypeScript catches misbinding at compile time, but
 * JavaScript callers from older versions can silently pass a `sessionId`
 * where `tenantId` is now expected, e.g.
 *   loadLatestHandoff(root, 'sess-abc')   // WRONG: 'sess-abc' becomes the tenant
 * which would silently filter to a non-existent tenant and return null with
 * no error. This guard rejects the most common shape of that mistake (any
 * value beginning with the conventional `sess-` / `sess_` session prefix).
 *
 * False-positive cost: a tenant literally named `sess-...` will be rejected.
 * Acceptable tradeoff for catching the silent-leak class.
 */
export declare function assertTenantId(fnName: string, value: unknown): asserts value is string;
export declare function saveActiveTaskSnapshot(hippoRoot: string, tenantId: string, snapshot: {
    task: string;
    summary: string;
    next_step: string;
    source?: string;
    session_id?: string | null;
    scope?: string | null;
}): TaskSnapshot;
export declare function loadActiveTaskSnapshot(hippoRoot: string, tenantId: string): TaskSnapshot | null;
export declare function clearActiveTaskSnapshot(hippoRoot: string, tenantId: string, clearedStatus?: string): boolean;
export declare function appendSessionEvent(hippoRoot: string, tenantId: string, event: {
    session_id: string;
    event_type: string;
    content: string;
    task?: string | null;
    source?: string;
    scope?: string | null;
    metadata?: Record<string, unknown>;
}): SessionEvent;
export declare function listSessionEvents(hippoRoot: string, tenantId: string, options?: {
    session_id?: string;
    task?: string;
    limit?: number;
}): SessionEvent[];
/**
 * Return session_ids with a `session_complete` event newer than `sinceMs`.
 * Used by the sleep auto-promotion pass to bound scanning to a fixed window.
 */
export declare function findPromotableSessions(hippoRoot: string, tenantId: string, sinceMs: number): Array<{
    session_id: string;
}>;
/**
 * Idempotency guard — true if a trace-layer memory with this source_session_id
 * already exists.
 */
export declare function traceExistsForSession(hippoRoot: string, tenantId: string, session_id: string): boolean;
export declare function listMemoryConflicts(hippoRoot: string, status?: string, tenantId?: string): MemoryConflict[];
export declare function replaceDetectedConflicts(hippoRoot: string, detected: Array<{
    memory_a_id: string;
    memory_b_id: string;
    reason: string;
    score: number;
}>, detectedAt?: string): void;
/**
 * Resolve a conflict by keeping one memory and weakening the other.
 * Sets conflict status to 'resolved' and halves the loser's half-life.
 * If --forget is used, the loser is deleted entirely.
 *
 * Returns the resolved conflict, or null if not found.
 */
export declare function resolveConflict(hippoRoot: string, conflictId: number, keepId: string, forgetLoser?: boolean, tenantId?: string): {
    conflict: MemoryConflict;
    loserId: string;
} | null;
/**
 * Save a session handoff record. Returns the persisted handoff.
 */
export declare function saveSessionHandoff(hippoRoot: string, tenantId: string, handoff: Omit<SessionHandoff, 'updatedAt'>): SessionHandoff;
/**
 * Load the most recent handoff, optionally filtered by session ID.
 */
export declare function loadLatestHandoff(hippoRoot: string, tenantId: string, sessionId?: string): SessionHandoff | null;
/**
 * Load a specific handoff by its row ID.
 */
export declare function loadHandoffById(hippoRoot: string, tenantId: string, id: number): SessionHandoff | null;
/**
 * Load summaries flagged dirty for the given tenant. Sorted by latest_at
 * DESC (NULLS LAST) so E3's rebuild cap (HIPPO_DAG_REBUILD_CAP, default 20)
 * takes the most-recently-changed summaries first.
 *
 * Returns full MemoryEntry shape via MEMORY_SELECT_COLUMNS + rowToEntry
 * (v28 fields are part of the standard read path).
 */
export declare function loadDirtySummaries(hippoRoot: string, tenantId: string): MemoryEntry[];
/**
 * v0.30 / E2 — in-transaction variant of markSummaryDirty. Takes an open
 * db (caller is responsible for any SAVEPOINT/BEGIN). Used by E2's hook
 * sites: writeEntryDbOnly, api.supersede CAS, deleteEntry, archiveRawMemory,
 * batchWriteAndDelete. Each child mutation's dirty-mark is atomic with the
 * mutation itself (where the mutation IS in a SAVEPOINT/BEGIN — deleteEntry
 * is the exception, acceptably non-atomic by design).
 *
 * EXPORTED (required for cross-module use by api.ts + raw-archive.ts).
 * Risk of misuse (caller without open tx) is mitigated by the InTx
 * suffix + the DatabaseSyncLike typed param. Public surface for end users
 * stays at the markSummaryDirty (own-connection) variant.
 *
 * Same idempotency contract: 0->1 transition only, audit row only on
 * transition, no-op on non-summary / archived / unknown id / cross-tenant.
 */
export declare function markSummaryDirtyInTx(db: DatabaseSyncLike, summaryId: string, tenantId: string, actor: string): void;
/**
 * Mark a summary as dirty. Idempotent (re-marking dirty is a no-op + no
 * second audit row). Tenant-scoped to prevent cross-tenant writes via
 * parent-lookup. Called by E2 from invalidation.ts / writeEntry /
 * forgetMemory / archiveRawMemory whenever a child is invalidated,
 * superseded, forgotten, or archived.
 *
 * Quietly no-ops if the target row doesn't exist or isn't a level-2
 * summary (E5 will widen the dag_level guard to IN (2, 3) when level-3
 * build path lands). Emits a 'summary_marked_dirty' audit row on actual
 * state transitions (0 -> 1) via the audit() helper, which try/catches
 * for missing audit_log (the v27 self-heal scenario).
 */
export declare function markSummaryDirty(hippoRoot: string, summaryId: string, tenantId: string, actor?: string): void;
/**
 * v0.30 / E5 — host-wide loader for L2 topic summaries without an L3 parent.
 * Used by consolidate phase 1.9 (buildEntityProfiles) to cluster L2s into
 * L3 entity profiles. Mirrors loadAllDirtySummaries pattern (E3): SQL-level
 * filter is cheaper than reusing in-memory `survivors` (which doesn't
 * contain L2s freshly created by phase 1.7 buildDag).
 *
 * Returns entries with tenantId attached so per-cluster writes stay
 * tenant-scoped via summary.tenantId.
 */
export declare function loadAllL2Summaries(hippoRoot: string): MemoryEntry[];
/**
 * v0.30 / E3 — host-wide variant of loadDirtySummaries. Iterates all tenants
 * in one query so consolidate.ts (host-wide per L106-109) does not need a
 * per-tenant loop. Each returned MemoryEntry carries its own tenantId (via
 * rowToEntry), so per-summary children + rebuild UPDATE stay tenant-scoped.
 *
 * Sort: latest_at DESC NULLS LAST, id ASC — same as per-tenant variant so
 * HIPPO_DAG_REBUILD_CAP takes most-recently-changed summaries first.
 */
export declare function loadAllDirtySummaries(hippoRoot: string): MemoryEntry[];
/**
 * v0.30 / E3 — load live children of a DAG summary. Used by
 * rebuildDirtySummaries to regenerate content from the CURRENT child set
 * (not the children at create-time). Skips archived. Tenant-scoped
 * (defence in depth — dag_parent_id is unique-ish but tenant guard is
 * cheap). created column is TEXT NOT NULL since db.ts schema v1.
 */
export declare function loadChildrenOfSummary(hippoRoot: string, summaryId: string, tenantId: string): MemoryEntry[];
/**
 * v0.30 / E3 — patch applied by applyRebuildResult. Two-branch shape
 * (bumpRebuildCount false for zero-child case, true for normal rebuild).
 */
export interface RebuildPatch {
    content: string;
    descendant_count: number;
    earliest_at: string | null;
    latest_at: string | null;
    bumpRebuildCount: boolean;
    zeroChildren: boolean;
    actor: string;
}
/**
 * v0.30 / E3 — apply a rebuild result to a dirty summary. Atomic: one
 * prepared UPDATE statement plus syncFtsRow inside one SAVEPOINT.
 * WHERE includes `AND summary_dirty = 1` so concurrent sleep's race-loser
 * becomes a no-op (no rebuild_count bump, no audit row).
 *
 * Returns true on actual rebuild (changes > 0), false on race-loss /
 * unknown id / archived / wrong dag_level.
 */
export declare function applyRebuildResult(hippoRoot: string, summary: MemoryEntry, patch: RebuildPatch): boolean;
/**
 * v0.30 / E3 — clear summary_dirty on a freshly-built summary. Called by
 * buildDag immediately after the child-link loop finishes. Without this,
 * each member's writeEntry call fires markSummaryDirtyInTx on the just-
 * created parent (E2 hook at store.ts:1214), and the same sleep cycle's
 * E3 rebuild phase would re-rebuild every new summary (2x LLM cost).
 *
 * Idempotent: no-op + no audit if summary isn't dirty. Audit
 * source='buildDag-clean' distinguishes from E3-rebuild source.
 */
export declare function clearSummaryDirtyAfterBuild(hippoRoot: string, summaryId: string, tenantId: string, actor?: string, source?: string): void;
export { getHippoDbPath };
//# sourceMappingURL=store.d.ts.map