/**
 * SQLite-backed project/session/message storage for `spectral serve`.
 *
 * Two-tier model:
 *   projects ─< sessions ─< messages
 *
 * Single-process, single-file. Using `better-sqlite3` because:
 *  - Synchronous API is a perfect fit for the per-WS-event write pattern.
 *  - No connection pool, no callback churn, transactions are trivial.
 *  - Native module ships prebuilt binaries for every Node version we support.
 *
 * Schema migrations:
 *   We track schema with the `user_version` PRAGMA. When opening a DB whose
 *   version does not match `SCHEMA_VERSION`, we DROP every known table and
 *   recreate. This is safe pre-1.0 because the agent UI is local-only and
 *   the data is conversation history we explicitly opted not to migrate
 *   for the project-tier rollout. Anything newer (real users with real
 *   data) needs a proper migration table.
 *
 * Foreign keys are enabled so DELETE FROM projects cascades to sessions
 * cascades to messages. The route layer must still tear down any in-flight
 * agent streams BEFORE deleting — see `SessionStreamManager.disposeProjectStreams`.
 *
 * The repo is intentionally a thin wrapper. We do NOT expose `Database`
 * itself — callers go through the typed methods so we can swap the backend
 * later (e.g. if we ever need to colocate with a remote service).
 */
import type { ImageAttachment, WireMessage, WireProject, WireSessionDetail, WireSessionSummary } from "./wire.js";
import type { SupportedMemoryDetails } from "../memory/types.js";
/** A single message as stored in SQLite. Identical to WireMessage. */
export type StoredMessage = WireMessage;
/** Input shape for `appendMessage`. */
export interface AppendMessageInput {
    /** Optional override; otherwise a UUID is generated. */
    id?: string;
    role: "user" | "assistant" | "system";
    content: string;
    /** Already-serialized JSONL of streamed events. Default "". */
    eventsJsonl?: string;
    /** Optional override; otherwise Date.now(). */
    createdAt?: number;
    /** Base64-encoded images attached to user messages (omitted for assistant/system). */
    images?: ImageAttachment[];
}
export interface CreateSessionInput {
    /** Owning project. Required — sessions cannot exist standalone. */
    projectId: string;
    /** Optional override; otherwise a UUID is generated. */
    id?: string;
    /** Optional title; default "New conversation". */
    title?: string;
}
export interface CreateProjectInput {
    /** Optional override; otherwise a UUID is generated. */
    id?: string;
    name: string;
    /** Absolute, validated filesystem path. */
    path: string;
}
export interface SessionMemorySnapshot {
    sessionId: string;
    summary: string;
    details: SupportedMemoryDetails;
    coveredSourceCount: number;
    updatedAt: number;
}
export interface UpdateProjectInput {
    name?: string;
    /** Already-validated absolute path. */
    path?: string;
}
interface QueueRow {
    id: string;
    session_id: string;
    content: string;
    position: number;
    created_at: string;
}
export declare class SessionStore {
    readonly path: string;
    private db;
    private closed;
    private stmtListProjects;
    private stmtGetProject;
    private stmtCreateProject;
    private stmtUpdateProject;
    private stmtDeleteProject;
    private stmtListSessionsByProject;
    private stmtListSessionIdsByProject;
    private stmtGetSession;
    private stmtCreateSession;
    private stmtDeleteSession;
    private stmtListMessages;
    private stmtAppendMessage;
    private stmtDeleteMessage;
    private stmtDeleteMessagesBySession;
    private stmtTouchSession;
    private stmtRenameSession;
    private stmtSessionWithCount;
    private stmtGetSessionMemorySnapshot;
    private stmtUpsertSessionMemorySnapshot;
    private stmtDeleteSessionMemorySnapshot;
    private stmtGetSessionModel;
    private stmtSetSessionModel;
    private stmtGetSessionReasoningEffort;
    private stmtSetSessionReasoningEffort;
    private stmtSetForkCompactSource;
    private stmtGetForkCompactSource;
    private stmtInsertProjectObs;
    private stmtSearchProjectObs;
    private stmtGetProjectByCwd;
    private stmtEnqueuePrompt;
    private stmtGetPromptQueue;
    private stmtDequeuePrompt;
    private stmtDeleteQueueItem;
    private stmtClearSessionQueue;
    private stmtShiftPositions;
    constructor(path: string);
    /** Smoke check: returns the names of the tables in the DB. */
    listTables(): string[];
    listProjects(): WireProject[];
    createProject(input: CreateProjectInput): WireProject;
    /** Returns null if not found. */
    getProject(id: string): WireProject | null;
    /**
     * Update a project's name and/or path. At least one must be provided.
     * Returns the updated project, or null if not found. Empty `name` falls
     * back to "Untitled project" for parity with createProject.
     */
    updateProject(id: string, input: UpdateProjectInput): WireProject | null;
    /**
     * Delete a project. Cascades to sessions and messages via FK.
     * Caller MUST tear down any active SessionStream subscribers / spectral
     * processes for sessions in this project BEFORE invoking this.
     * Returns the list of session ids that belonged to the project (so
     * the caller can include them in the stream-teardown call).
     */
    deleteProject(id: string): {
        deleted: boolean;
        sessionIds: string[];
    };
    /** Sessions belonging to a single project, newest-first. */
    listSessionsByProject(projectId: string): WireSessionSummary[];
    createSession(input: CreateSessionInput): WireSessionSummary;
    /** Returns null if not found. */
    getSession(id: string): WireSessionDetail | null;
    /** Convenience for routes/managers that just need the projectId. */
    getSessionProjectId(id: string): string | null;
    getMessages(sessionId: string): StoredMessage[];
    /**
     * Append a message and bump the session's updated_at in one transaction.
     * Throws if the session doesn't exist (FK violation).
     */
    /** Delete a message by id. No-op if the message doesn't exist. */
    deleteMessage(id: string): void;
    /** Delete all persisted messages for a session. */
    deleteMessagesBySession(sessionId: string): void;
    appendMessage(sessionId: string, msg: AppendMessageInput): StoredMessage;
    /**
     * Rename a session. Bumps `updated_at`. Returns the updated summary, or
     * null if the session doesn't exist. Title is trimmed; an empty title falls
     * back to "New conversation" for parity with createSession().
     */
    renameSession(id: string, title: string): WireSessionSummary | null;
    /** Returns true if a row was deleted. Cascades to messages. */
    deleteSession(id: string): boolean;
    getSessionMemorySnapshot(sessionId: string): SessionMemorySnapshot | null;
    upsertSessionMemorySnapshot(sessionId: string, snapshot: Omit<SessionMemorySnapshot, "sessionId" | "updatedAt">): void;
    deleteSessionMemorySnapshot(sessionId: string): void;
    /**
     * Get the persisted modelId for a session, or null if none was ever set or
     * the session does not exist. The CLI uses this for cross-restart recovery:
     * when an envelope arrives WITHOUT a `modelId` but SQLite has a value
     * persisted from an earlier turn, we apply the persisted value before
     * forwarding to spectral. When neither envelope nor SQLite have a value, we
     * leave model selection to spectral's own settings file (pre-Phase-3 behaviour).
     */
    getSessionModel(sessionId: string): string | null;
    /**
     * Persist the modelId for a session. Pass `null` to clear. Does NOT bump
     * `updated_at` — model selection is metadata, not user activity, so it
     * shouldn't promote a session to the top of the sidebar. Silently no-ops
     * when the session does not exist (the caller has already validated the
     * sessionId via attach/getSession in the normal flow).
     */
    setSessionModel(sessionId: string, modelId: string | null): void;
    /**
     * Get the persisted reasoningEffort for a session, or null if none
     * was ever set. Returns null for unknown sessions (consistent with
     * getSessionModel's contract).
     */
    getSessionReasoningEffort(sessionId: string): string | null;
    /**
     * Persist the reasoningEffort for a session. Pass `null` to clear.
     * Same semantics as setSessionModel — best-effort metadata, does
     * not bump `updated_at`.
     */
    setSessionReasoningEffort(sessionId: string, effort: string | null): void;
    /**
     * Fork a session: create a new session in the same project, copy all
     * messages from the source, and set the `fork_compact_source_id` flag
     * so SessionStreamManager compacts the context after the first assistant
     * turn completes.
     */
    forkSession(sourceId: string, opts?: {
        title?: string;
        newSessionId?: string;
    }): WireSessionSummary;
    /**
     * Read the fork-compact source id for a session, or null if the session
     * was not forked or has already been compacted.
     */
    getForkCompactSource(sessionId: string): string | null;
    /**
     * Clear the fork-compact flag after compaction completes (or fails).
     * Idempotent-safe.
     */
    clearForkCompactSource(sessionId: string): void;
    /**
     * Insert multiple project observations in a single transaction.
     * Uses INSERT OR REPLACE so re-running after the same compaction is idempotent.
     */
    insertProjectObservations(projectId: string, sessionId: string, observations: Array<{
        id: string;
        content: string;
        relevance: string;
    }>, createdAt: number): void;
    /**
     * Search project observations by substring match. Returns up to 20 most recent.
     */
    searchProjectObservations(projectId: string, query: string, limit?: number): Array<{
        content: string;
        relevance: string;
        createdAt: number;
        sessionId: string;
    }>;
    /**
     * Look up a project by its absolute filesystem path.
     * Returns null when no project has been registered for the given path.
     */
    getProjectByCwd(cwd: string): string | null;
    /**
     * Enqueue a prompt for a session. Returns the created queue item.
     * Position is auto-assigned as (max existing position + 1).
     */
    enqueuePrompt(sessionId: string, content: string): QueueRow;
    /**
     * Dequeue (remove and return) the first prompt for a session.
     * Returns null if the queue is empty.
     * Renumbers remaining items so positions stay contiguous from 0.
     */
    dequeuePrompt(sessionId: string): QueueRow | null;
    /**
     * Get the full prompt queue for a session, ordered by position.
     */
    getPromptQueue(sessionId: string): QueueRow[];
    /**
     * Remove a specific prompt from the queue by id.
     * Renumbers remaining items so positions stay contiguous.
     */
    removePrompt(sessionId: string, itemId: string): void;
    /**
     * Clear the entire prompt queue for a session.
     */
    clearPromptQueue(sessionId: string): void;
    close(): void;
}
/**
 * Pre-flight: verify that better-sqlite3 loads and a writable DB can be opened.
 *
 * Called before binding the port so a broken native build fails loudly with
 * a helpful hint instead of crashing later when the first WS connection
 * arrives.
 *
 * Implementation note: we test by opening the *real* target path. Opening a
 * tmpfile would only verify that the native module loads, not that the
 * config dir is writable. Since `open()` already creates the file/dir
 * idempotently, this is safe to call repeatedly.
 */
export interface PreflightResult {
    ok: boolean;
    /** Human-friendly error message ready to print. Set when ok=false. */
    error?: string;
}
export declare function preflightSqlite(dbPath: string): PreflightResult;
export {};
//# sourceMappingURL=storage.d.ts.map