/**
 * CoherenceJournal — per-machine, per-kind append-only event streams
 * (P1.1 of multi-machine coherence).
 *
 * Spec: docs/specs/COHERENCE-JOURNAL-SPEC.md §3.1 (writer rules), §3.2 (typed
 * schemas), §3.7 (per-kind retention).
 *
 * This class is ONLY the writer + a minimal tolerant reader for the read API
 * (built later). It performs its OWN file I/O — it is not a StateManager
 * method — so the standby read-only guard is an injected seam
 * (`guardWrite?: (path) => void`); another workstream wires the real
 * `StateManager.guardJournalWrite` entrypoint into it.
 *
 * The load-bearing safety rule (§3.1): emit() is a NON-BLOCKING memory
 * operation. It validates + enqueues + returns in microseconds; a background
 * flusher drains the queue with single-line O_APPEND writes and batched
 * fdatasync on a cadence (default 250ms). NO synchronous I/O ever runs in a
 * caller's stack — the host has a documented event-loop-starvation history and
 * a blocking fsync at a placement/session/autonomous code path would reproduce
 * the originating incident class.
 *
 * Crash-safe meta ordering (§3.4 rule 3): data lines are appended +
 * fdatasync'd FIRST, then `meta.highWaterSeq` is advanced via atomic temp-file
 * rename — so `durable_tail >= highWaterSeq` holds at every instant, including
 * a kill-9 between the two steps. The incarnation token is re-minted IFF on
 * open the file's last seq is strictly below `meta.highWaterSeq` (a genuine
 * rewind / restore-from-backup); a trailing-partial-line repair is NEVER a
 * re-mint.
 *
 * The journal subsystem never emits journal events about itself.
 */
import fs from 'node:fs';
import { type ReplicatedKindRegistry } from './ReplicatedRecordEnvelope.js';
export type JournalKind = 'topic-placement' | 'session-lifecycle' | 'autonomous-run' | 'threadline-conversation' | 'guard-latch' | 'pref-record' | 'relationship-record' | 'learning-record' | 'knowledge-record' | 'evolution-action-record' | 'user-record' | 'topic-operator-record' | 'threadline-pairing-record' | 'subscription-account-meta';
export declare const JOURNAL_KINDS: JournalKind[];
/** §3.2 enums. */
export type PlacementReason = 'user-move' | 'placed' | 'failover' | 'released' | 'quota-block-move' | 'reconcile';
export type SessionStatus = 'created' | 'completed' | 'killed' | 'reaped' | 'failed';
export type AutonomousAction = 'started' | 'stopped';
/** P3 (THREADLINE-CONVERSATION-COHERENCE-SPEC §3.1). */
export type ThreadlineConversationAction = 'started' | 'bound' | 'unbound' | 'closed';
/**
 * guard-latch (green-pr-automerge-enforcement R9/R7). A pool-visible latch or
 * marker that gates a guarded autonomous authority across the machine pool. The
 * `latchKind` namespaces independent latch families so they cannot collide; the
 * `action` is set/clear. ABSORBING ordering (set wins ties regardless of epoch)
 * is resolved by the consumer (GuardLatchStore), NOT here — the journal only
 * carries the content-free fact that a transition occurred.
 */
export type GuardLatchAction = 'set' | 'clear';
export interface PlacementData {
    owner: string;
    prevOwner?: string;
    epoch: number;
    reason: PlacementReason;
}
export interface SessionLifecycleData {
    sessionId: string;
    status: SessionStatus;
    reapReason?: string;
    reapLogRef?: string;
}
export interface AutonomousRunData {
    action: AutonomousAction;
    runId: string;
    artifactPaths: string[];
}
/** P3 §3.1 — content-free conversation lifecycle (no titles, no text). */
export interface ThreadlineConversationData {
    action: ThreadlineConversationAction;
    conversationId: string;
    peerFingerprint: string;
    topicId?: number;
}
/**
 * guard-latch (green-pr-automerge-enforcement R9/R7). Content-free: the latch
 * family, the set/clear action, the lease epoch + a monotonic sequence the
 * consumer uses for ordering, and a stable `latchId` so `/enable` can clear a
 * SPECIFIC latch without touching siblings. No free text (the typed-schema
 * invariant); `reason` is a short enum-like slug, length-capped.
 */
export interface GuardLatchData {
    latchKind: string;
    latchId: string;
    action: GuardLatchAction;
    epoch: number;
    seq: number;
    reason?: string;
}
/** One line in a stream file. */
export interface JournalEntry {
    seq: number;
    ts: string;
    machine: string;
    kind: JournalKind;
    topic?: number;
    data: Record<string, unknown>;
}
/** §3.7 per-kind retention. */
export interface KindRetention {
    maxFileBytes: number;
    /** N>0 = rotate at maxFileBytes, keep N archives, delete older. 0 = rotate but NEVER delete. */
    rotateKeep: number;
}
export declare const DEFAULT_RETENTION: Record<JournalKind, KindRetention>;
export declare const DEFAULT_FLUSH_INTERVAL_MS = 250;
export declare const DEFAULT_MAX_ENTRY_BYTES: number;
/**
 * Per-entry byte cap for a REPLICATED `*-record` kind (WS2 send-side). A
 * disclosure-minimized record's `data` is capped at 64 KB by its store builder
 * (e.g. LEARNING_MAX_ENTRY_BYTES); the serialized journal LINE adds the entry
 * envelope (seq/ts/machine/kind), so the line cap must sit a margin ABOVE the
 * data cap or a legal-but-fat record would be dropped as oversize. 80 KB clears
 * the 64 KB data ceiling with comfortable headroom while staying bounded. The
 * applier's receive-side cap (JournalSyncApplier) uses the SAME constant so a
 * record the writer emits can never be rejected as oversize on receive. */
export declare const REPLICATED_RECORD_MAX_ENTRY_BYTES: number;
/** Tail-scan window for op-key dedupe reconstruction (§3.1). */
export declare const DEDUPE_WINDOW = 200;
/** Token-bucket defaults per kind (emits/sec sustained; burst = capacity). */
export declare const DEFAULT_RATE_CAP: {
    capacity: number;
    refillPerSec: number;
};
/** Reverse-tail read byte ceiling for the tolerant reader. */
export declare const DEFAULT_READ_BYTE_CEILING: number;
export interface RateCapConfig {
    capacity: number;
    refillPerSec: number;
}
export interface CoherenceJournalConfig {
    /** Absolute path to the agent's `.instar/` directory (the stateDir). */
    stateDir: string;
    /** Stable id for THIS machine (the single producer of its own streams). */
    machineId: string;
    /** Flush cadence (ms). Default 250. */
    flushIntervalMs?: number;
    /** Per-entry size cap (bytes). Default 8KB. */
    maxEntryBytes?: number;
    /** Per-kind retention overrides; missing kinds fall back to DEFAULT_RETENTION. */
    retention?: Partial<Record<JournalKind, KindRetention>>;
    /** Per-kind rate cap (token bucket). Default applied to every kind. */
    rateCap?: RateCapConfig;
    /**
     * Allowlisted roots that artifactPaths must canonicalize under (§3.1 write-time
     * jail). Absolute paths. Defaults to [`.instar/autonomous/`, the stateDir].
     */
    artifactRoots?: string[];
    /**
     * Injected standby read-only guard (§3.1). The flusher calls this with the
     * target file path before each append batch; throwing skips the batch + counts.
     * Another workstream wires the real `StateManager.guardJournalWrite`.
     */
    guardWrite?: (filePath: string) => void;
    /** Optional clock override (tests). */
    now?: () => Date;
    /** Optional logger; called once per failure class. */
    logger?: (msg: string) => void;
    /**
     * Optional fs seam for fault-injection tests (wedge the flusher). Defaults to
     * node:fs primitives. Only the primitives the flusher uses are seamed.
     */
    fsImpl?: JournalFs;
}
/** The fs primitives the flusher/opener use (seamable for fault injection). */
export interface JournalFs {
    /** Optional (lock mtime heartbeat, #925). Defaults to fs.futimesSync. */
    futimesSync?: (fd: number, atime: Date, mtime: Date) => void;
    openSync: typeof fs.openSync;
    writeSync: typeof fs.writeSync;
    fdatasyncSync: typeof fs.fdatasyncSync;
    closeSync: typeof fs.closeSync;
    existsSync: typeof fs.existsSync;
    statSync: typeof fs.statSync;
    renameSync: typeof fs.renameSync;
    writeFileSync: typeof fs.writeFileSync;
    readFileSync: typeof fs.readFileSync;
    readdirSync: typeof fs.readdirSync;
    truncateSync: typeof fs.truncateSync;
    mkdirSync: typeof fs.mkdirSync;
    openReadSync?: typeof fs.openSync;
    readSync: typeof fs.readSync;
}
/** Degradation counters surfaced to the read API (§3.1). */
export interface JournalDegradation {
    /** Trailing-partial-line truncations on open (Distrust Temporary Success). */
    repairs: number;
    /** Genuine rewinds detected (last seq < highWaterSeq) → re-mint. */
    remints: number;
    /** Entries dropped for failing the typed schema. */
    schemaRejects: number;
    /** Unknown fields dropped from otherwise-valid data. */
    droppedFields: number;
    /** Entries dropped for exceeding the per-entry size cap. */
    oversize: number;
    /** Emits dropped by the rate cap. */
    rateLimited: number;
    /** artifactPaths rejected by the write-time jail. */
    jailRejects: number;
    /** Append batches skipped because guardWrite threw (read-only standby). */
    guardSkips: number;
    /** Flush failures (write/fsync errors). */
    flushErrors: number;
    /** Idempotent emits collapsed by the op-key dedupe. */
    dedupeHits: number;
}
/** Per-stream status surfaced to the read API. */
export interface StreamStatus {
    kind: JournalKind;
    machine: string;
    incarnation: string;
    lastSeq: number;
    highWaterSeq: number;
}
/** Result of a tolerant reverse-tail read. */
export interface TolerantReadResult {
    entries: JournalEntry[];
    skippedCorrupt: number;
    truncated: boolean;
}
type WriterState = 'active' | 'writer-locked-out' | 'closed';
/**
 * Sanitize a machine id for use in a file name. Mirrors MachineHeartbeat:
 * percent-encode anything outside [A-Za-z0-9_-] so a stray slash or `..`
 * cannot escape, and the mapping is injective + traversal-safe.
 */
export declare function sanitizeMachineId(machineId: string): string;
export declare class CoherenceJournal {
    private readonly stateDir;
    private readonly machineId;
    private readonly safeMachineId;
    private readonly flushIntervalMs;
    private readonly maxEntryBytes;
    private readonly retention;
    private readonly rateCapCfg;
    private readonly artifactRoots;
    private readonly guardWrite?;
    private readonly now;
    private readonly logger?;
    private readonly io;
    /**
     * The replicated-kind registry (WS2 send-side). Injected via
     * setReplicatedKindRegistry so the journal can validate + accept a registered
     * `*-record` kind's envelope. ABSENT ⇒ unchanged behavior (only the 5 hardcoded
     * lifecycle kinds validate; a `*-record` kind schema-rejects exactly as before
     * this seam landed). The journal never depends on the registry being present.
     */
    private replicatedRegistry?;
    private state;
    private incarnation;
    /** Next seq to assign at enqueue, per kind (in-memory counter seeded at open). */
    private nextSeq;
    /** Durable highWaterSeq per kind (advanced after data fdatasync). */
    private highWaterSeq;
    /** In-memory enqueue order; drained by the flusher in seq order per kind. */
    private queue;
    /** Recent-window op-key set per kind (reconstructed on open by tail-scan). */
    private opKeys;
    private rateBuckets;
    private timer;
    /** Lock-retry timer while writer-locked-out (issue #925). */
    private retryTimer;
    /** Flush counter for the lock mtime heartbeat (pid-reuse defense, #925). */
    private flushCount;
    private lockFd;
    private flushing;
    /** Monotonic suffix so rapid rotations within one ms get unique archive names. */
    private rotationCounter;
    private degradation;
    /** Has the one-time repair signal already fired this lifetime? */
    private repairSignalled;
    private loggedClasses;
    constructor(config: CoherenceJournalConfig);
    /**
     * Acquire the single-process lock, recover any torn trailing lines, seed the
     * seq counters + op-key index, and start the background flusher. Idempotent —
     * a second `open()` on an already-open instance is a no-op.
     */
    open(): void;
    /** Acquire + recover + start the flusher. Returns false when the lock is held. */
    private tryActivate;
    /** Stop the flusher, drain once synchronously (best-effort), release the lock. */
    close(): void;
    /** Writer status (queryable from the instance per §3.1). */
    get status(): WriterState;
    get isLockedOut(): boolean;
    getDegradation(): Readonly<JournalDegradation>;
    /** Per-stream status (incarnation, lastSeq, highWaterSeq) for the read API. */
    streamStatuses(): StreamStatus[];
    /**
     * The OWN-stream replication advert for the journal-sync transport (§3.4 rule
     * 5): per kind, the incarnation + the highest DURABLY-FLUSHED seq this writer
     * can serve. Unlike `streamStatuses().lastSeq`, this advertises `highWaterSeq`
     * (advanced only after data fdatasync) — NEVER an enqueued-but-unflushed seq —
     * so a peer never requests a delta we cannot serve from the file. Returns `{}`
     * with a zeroed entry per kind when nothing has been flushed yet; callers may
     * forward it verbatim (old peers ignore unknown fields).
     */
    getOwnAdvert(): Record<JournalKind, {
        incarnation: string;
        lastSeq: number;
    }>;
    /** Number of entries enqueued but not yet flushed (tests / observability). */
    get pendingCount(): number;
    /** topic-placement (§3.2). Op key: (topic, epoch). */
    emitPlacement(topic: number, data: PlacementData): void;
    /** session-lifecycle (§3.2). Op key: (sessionId, status). topic optional. */
    emitLifecycle(data: SessionLifecycleData, topic?: number): void;
    /** autonomous-run (§3.2). Op key: (topic, runId, action). */
    emitAutonomousRun(topic: number, data: AutonomousRunData): void;
    /** threadline-conversation (P3 §3.1). Op key: (conversationId, action, topicId?). */
    emitThreadlineConversation(data: ThreadlineConversationData): void;
    /**
     * guard-latch (green-pr-automerge R9/R7). Op key: (latchKind, latchId, seq) —
     * each transition carries a fresh monotonic `seq`, so distinct transitions are
     * never deduped away while a genuine retry of the SAME (kind,id,seq) collapses.
     */
    emitGuardLatch(data: GuardLatchData): void;
    /**
     * Inject the replicated-kind registry (WS2 send-side). Idempotent + optional:
     * the journal validates a registered `*-record` kind's envelope via the store
     * schema the registry carries; absent ⇒ unchanged behavior. Called once at
     * wiring time (server.ts), after the concrete stores register their kinds.
     */
    setReplicatedKindRegistry(registry: ReplicatedKindRegistry | undefined): void;
    /**
     * Emit a replicated-store record (WS2 send-side). `data` MUST be a built,
     * disclosure-minimized envelope `data` (the store's `build*RecordData` output)
     * carrying `recordKey`/`hlc`/`op`/`origin` (+ optional `observed`). The op-key
     * is `recordKey:serializeHlcKey(hlc)` so a retry of the EXACT logical event
     * (same key + same HLC) dedupes, while a new HLC is a distinct event — the §4
     * idempotency rule. Validation runs in the shared `emit()` path (which delegates
     * a registered replicated kind to `validateReplicatedEnvelope`), so a malformed
     * record is a counted schema-reject, never a throw. Non-blocking; never throws
     * into the caller (the manager emit hooks are best-effort). A no-op when the
     * kind is not a registered replicated kind (the registry is the authority).
     */
    emitReplicatedRecord(kind: JournalKind, data: Record<string, unknown>): void;
    /** Derive the restart-proof op-key for a replicated record: `recordKey:hlcKey`.
     *  Best-effort — a malformed key/hlc still yields a stable string (the record is
     *  then rejected by validate(), so the op-key only matters for well-formed ones). */
    private replicatedOpKey;
    /** The per-entry byte cap for a kind — RAISED for replicated `*-record` kinds
     *  (a disclosure-minimized record can exceed the 8 KB lifecycle cap; §4). */
    private maxEntryBytesForKind;
    /**
     * Generic non-blocking emit: validate + jail + dedupe + rate-cap + serialize,
     * assign a seq, enqueue, and RETURN. No synchronous file I/O on this stack —
     * the flusher does all of it. Never throws into the caller.
     */
    private emit;
    /**
     * §3.2 typed schema. Returns the validated `data` object (only known fields,
     * enums enforced) or null to reject. Unknown fields are dropped + counted.
     */
    private validate;
    /**
     * §3.1 write-time jail. Each path must resolve (and, if it exists, realpath)
     * to a location contained under an allowlisted root. `..` segments, absolute
     * paths outside the jail, and symlink escapes are rejected. Returns the
     * canonicalized path list, or null if ANY path escapes (the whole entry is
     * then rejected + counted by the caller).
     */
    private jailArtifactPaths;
    /**
     * Returns the realpath-resolved candidate IF it is contained under an
     * allowlisted root (symlink-safe), else null. We realpath the deepest
     * existing ancestor and re-append the non-existent tail so a path that does
     * not exist yet still gets symlink-escape protection on its existing prefix.
     */
    private realpathContained;
    private isContained;
    private takeToken;
    /**
     * Drain the queue: per kind, append data lines (single-line O_APPEND writes),
     * batch fdatasync, THEN advance meta.highWaterSeq via atomic temp rename.
     * Crash-safe ordering: data durable BEFORE meta. Never throws.
     */
    flush(): void;
    private loadOrInitMeta;
    private persistMeta;
    private mintIncarnation;
    /**
     * Open a kind's current stream file: repair any torn trailing line, seed
     * `nextSeq` from the durable tail, reconstruct the op-key window, and apply
     * the incarnation re-mint rule (re-mint IFF last seq < highWaterSeq).
     */
    private recoverKind;
    /** Tail-scan the last DEDUPE_WINDOW lines to rebuild the op-key index. */
    private reconstructOpKeys;
    private opKeyOf;
    /** Keep the op-key window bounded (No Unbounded Loops). */
    private trimOpKeys;
    /**
     * Rotate the current file when it exceeds maxFileBytes. rotateKeep N>0 keeps
     * N archives + deletes older; rotateKeep 0 rotates but NEVER deletes. Seq
     * continues across rotation (the in-memory counter is untouched).
     */
    private maybeRotate;
    private pruneArchives;
    /** Archive file names for a kind, sorted oldest→newest by the embedded ts. */
    private listArchives;
    /** Enumerate this writer's own stream files present on disk (current + archives). */
    enumerateStreams(): {
        kind: JournalKind;
        file: string;
        isArchive: boolean;
    }[];
    /**
     * Tolerantly read up to `limit` most-recent entries from a kind's CURRENT
     * file via a bounded reverse tail read (per-line try/catch; corrupt lines are
     * skipped + counted). Reads at most `byteCeiling` bytes from the file end.
     */
    readTail(kind: JournalKind, limit?: number, byteCeiling?: number): TolerantReadResult;
    private acquireLock;
    /** Returns true (and removes the lock) if the recorded pid is dead. */
    private reclaimStaleLock;
    private releaseLock;
    private dirPath;
    private currentFilePath;
    private metaPath;
    private lockPath;
    private ensureDirs;
    private log;
}
/**
 * Standalone tolerant reverse-tail reader — shared by the writer's `readTail`
 * and (later) the read API's direct-file path. Reads at most `byteCeiling`
 * bytes from the end of `filePath`, parses lines newest-first, skips + counts
 * corrupt lines, and returns up to `limit` entries (newest first).
 */
export declare function readTailTolerant(io: JournalFs, filePath: string, limit: number, byteCeiling: number): TolerantReadResult;
export {};
//# sourceMappingURL=CoherenceJournal.d.ts.map