/**
 * SQLite-backed outbox for the background review agent.
 *
 * The review agent runs asynchronously after every analysis: a backend
 * crash, deploy, or process restart in between must NOT lose review jobs.
 * Filesystem-based queues (one JSON file per state) lose race exclusivity
 * once two workers contend for the same job. SQLite + WAL gives us atomic
 * `UPDATE … WHERE state='pending' RETURNING …` lease semantics in a single
 * statement, plus partial unique indexes for `dedupe_key` so a flapping
 * caller can't enqueue the same job twice.
 *
 * The DB lives at `<cwd>/data/self_improve/self_improve.db` — separate from
 * the sessions DB so a corrupt outbox never takes the analysis path down
 * with it.
 *
 * See docs/architecture/self-improving-design.md §7 (SQLite Outbox).
 */
import Database from 'better-sqlite3';
export type JobState = 'pending' | 'leased' | 'done' | 'failed';
export interface EnqueueInput {
    /**
     * Globally unique dedupe key. The recommended form is
     * `${sessionId}::${turnIndex}::${skillId}::${failureModeHash}` so the same
     * skill+failure combo on the same turn can't be enqueued twice while still
     * leaving room for legitimate retries on a different turn.
     */
    dedupeKey: string;
    /** Higher numbers run first. Default 0. */
    priority?: number;
    /** Arbitrary JSON-serializable payload — never mutated by the outbox. */
    payload: unknown;
}
export interface ReviewJob {
    id: string;
    state: JobState;
    dedupeKey: string;
    priority: number;
    attempts: number;
    leaseOwner: string | null;
    leaseUntil: number | null;
    createdAt: number;
    updatedAt: number;
    payload: unknown;
    lastError: string | null;
}
export interface OutboxOptions {
    /** Override default DB path for tests. Pass ':memory:' for an ephemeral store. */
    dbPath?: string;
}
export interface EnqueueResult {
    enqueued: boolean;
    jobId?: string;
    /** Latency of the enqueue insert in ms — surfaced for the metrics PR. */
    latencyMs: number;
    /**
     * Reason the enqueue was a no-op. `duplicate_active` means a pending or
     * leased job with the same dedupe key already exists.
     */
    reason?: 'duplicate_active' | 'error';
}
/**
 * Open the outbox DB, run migrations, and return a closure that exposes the
 * supported operations. The DB connection is owned by the caller — call
 * `close()` to release it.
 */
export declare function openReviewOutbox(opts?: OutboxOptions): ReviewOutboxHandle;
export declare class ReviewOutboxHandle {
    private readonly db;
    constructor(db: Database.Database);
    /**
     * Enqueue a new job. Returns `{ enqueued: false, reason: 'duplicate_active' }`
     * if a pending or leased job with the same dedupe key already exists; this is
     * a successful no-op, not an error, so callers don't need a try/catch.
     */
    enqueue(input: EnqueueInput): EnqueueResult;
    /**
     * Atomically pick the highest-priority pending job and mark it leased.
     * Returns null if the queue is empty (or every pending job is gated by
     * attempts ≥ maxAttempts).
     *
     * The lease is owned by `workerOwner` until `lease_until` — past that the
     * caller should call `expireStaleLeases()` to recycle the job back to
     * pending.
     */
    leaseNext(input: {
        workerOwner: string;
        leaseDurationMs?: number;
        maxAttempts?: number;
    }): ReviewJob | null;
    /** Mark a leased job as completed. Idempotent on a re-call (no-op). */
    markDone(jobId: string): void;
    /**
     * Mark a leased job failed. If `attempts < maxAttempts`, the job goes back
     * to pending so a future poll can retry; otherwise it lands in 'failed'
     * permanently. `lastError` is truncated to keep DB size bounded.
     */
    markFailed(jobId: string, lastError: string, maxAttempts?: number): void;
    /**
     * Voluntarily release a lease without consuming an attempt. Used by the
     * worker when it leased a job but immediately determined the job has to
     * wait (cooldown, blocked dependency). The job goes back to pending and
     * keeps its existing attempts counter.
     */
    releaseLease(jobId: string): void;
    /**
     * Recycle leased jobs whose lease has expired back to pending. Called by
     * the worker poll loop on each tick — cheap because the lease_until index
     * keeps the scan tight.
     */
    expireStaleLeases(now?: number): number;
    /** Return counts grouped by state — used by the monitoring PR. */
    countByState(): Record<JobState, number>;
    /**
     * Number of jobs created in the last 24h — feeds the daily-budget cap.
     * Uses created_at so retries/leases don't double-count.
     */
    dailyJobCount(now?: number): number;
    /** Test helper — returns the full job row for assertions. */
    getJob(jobId: string): ReviewJob | null;
    /** Latest applied schema version — for migration tests. */
    schemaVersion(): number;
    close(): void;
}
/** Surfaced for assertions without exporting the constants individually. */
export declare const __testing: {
    SCHEMA_VERSION_LATEST: number;
};
//# sourceMappingURL=reviewOutbox.d.ts.map