import { type ActiveSession } from './active.js';
/**
 * How long a snapshot may be served before a reader re-gathers.
 * Short on purpose: live status (running/idle/waiting) must not go stale.
 * One-shot readers may re-gather after this ceiling. Long-lived watchers never
 * do: they read one reset then tail the canonical writer journal.
 */
export declare const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS = 15000;
/** Snapshot scope: this host only, or a fleet-wide merge written by a reader. */
export type ActiveCacheScope = 'local' | 'fleet';
export interface ActiveSessionsSnapshot {
    version: 1;
    scope: ActiveCacheScope;
    /** Epoch ms the sessions array was captured. */
    capturedAt: number;
    sessions: ActiveSession[];
    /** Peer count from the last fleet gather (fleet scope only). */
    remoteDeviceCount?: number;
}
export interface ActiveSessionsJournalRecord {
    version: 1;
    scope: ActiveCacheScope;
    capturedAt: number;
    upserts: ActiveSession[];
    removes: string[];
}
export declare function activeSessionJournalIdentity(row: ActiveSession): string;
/**
 * Per-session fields that are stable until the transcript changes. Keyed on
 * transcript mtime so a rewrite invalidates them. Live status is intentionally
 * absent — see {@link LIVE_STATUS_KEYS}.
 */
export interface ImmutableSessionFields {
    topic?: string;
    firstUserMessage?: string;
    label?: string;
    name?: string;
    cwd?: string;
    project?: string | null;
    attachments?: ActiveSession['attachments'];
    startedAtMs?: number;
    version?: string;
    pr?: ActiveSession['pr'];
    worktree?: ActiveSession['worktree'];
    ticket?: ActiveSession['ticket'];
    createdTickets?: string[];
    spawnedTeam?: string;
    sessionFile?: string;
    owner?: string;
    assignedTask?: string;
    kind?: string;
    context?: ActiveSession['context'];
}
/** Keys stored in the immutable memo (transcript-stable). */
export declare const IMMUTABLE_FIELD_KEYS: readonly ["topic", "firstUserMessage", "label", "name", "cwd", "project", "attachments", "startedAtMs", "version", "pr", "worktree", "ticket", "createdTickets", "spawnedTeam", "sessionFile", "owner", "assignedTask", "kind", "context"];
/**
 * Live / volatile fields that MUST NOT be served from the immutable memo.
 * They either change without a transcript write (pid death, attach state) or
 * are short-window signals (preview, tok/s). The short snapshot TTL is the
 * only cache that may carry them — and only as a whole-row snapshot.
 */
export declare const LIVE_STATUS_KEYS: readonly ["status", "activity", "preview", "tokPerSec", "awaitingReason", "question", "todos", "tail", "lastActivityMs", "hostLink", "presence", "pidAlive", "tmuxClients", "windowHeartbeatMs", "provenance", "rateLimited", "plan"];
/**
 * How long after the last reader heartbeat the daemon considers the journal
 * to have no active consumer and skips the expensive `ps`+`lsof` gather.
 *
 * Must be significantly larger than {@link SESSION_WATCH_HEARTBEAT_MS} (15s)
 * so a single delayed heartbeat does not cause a false-idle. Three tick-lengths
 * gives adequate margin while still cutting load within ~45s of the last
 * watcher disconnecting.
 */
export declare const ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS = 45000;
/** Test seam: redirect the snapshot file. Returns the previous override. */
export declare function setActiveSessionsSnapshotPathForTest(p: string | null): string | null;
/** Test seam: isolate process-local entries between fixtures. */
export declare function clearActiveSnapshotMemoryForTest(): void;
/** Test seam: redirect the immutable-memo file. Returns the previous override. */
export declare function setImmutableMemoPathForTest(p: string | null): string | null;
/** Test seam: redirect the append-only live-state journal. */
export declare function setActiveSessionsJournalPathForTest(p: string | null): string | null;
/** Test seam: redirect the reader-presence file. Returns the previous override. */
export declare function setActiveSessionsReaderPresencePathForTest(p: string | null): string | null;
export declare function activeSessionsJournalPath(): string;
/**
 * Record that a journal consumer is active right now.
 *
 * Called by {@link watchLocalSessions} on startup and on each heartbeat so
 * the daemon warm tick can gate the expensive `ps`+`lsof` gather on reader
 * presence and skip it when no watcher has checked in recently.
 */
export declare function noteActiveSessionsJournalReader(nowMs?: number): void;
/**
 * True when a journal consumer has signalled its presence within
 * {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS}.
 *
 * Used by the daemon warm tick to decide whether to run the expensive
 * `ps`+`lsof` gather. On an idle box with no watcher the tick skips the
 * gather entirely; the moment a new watcher connects it calls
 * {@link noteActiveSessionsJournalReader} and the following tick gathers.
 */
export declare function isActiveSessionsJournalReaderRecent(nowMs?: number, idleWindowMs?: number): boolean;
/** Default poll cadence for {@link watchActiveSessionsReaderPresence}. */
export declare const ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS = 1000;
/**
 * Watch for a reader going from absent/idle to present and fire `onConnect`
 * out of band, instead of waiting for the next scheduled warm tick (RUSH-2484).
 *
 * {@link noteActiveSessionsJournalReader} only writes a timestamp — it has no
 * path back to the daemon process that owns the warm tick's `setInterval`. A
 * watcher connecting to a cold or long-idle daemon therefore used to wait up
 * to one full {@link ACTIVE_SESSIONS_WARM_TICK_MS} tick for its first fresh
 * rows. This polls the tiny presence file (not the expensive `ps`+`lsof`
 * gather) at {@link ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS} and calls
 * `onConnect` only on the idle→recent edge, so a steadily-connected reader's
 * repeated heartbeats never re-trigger it and an idle box with no reader never
 * gathers — the CPU win from RUSH-3193 is preserved.
 *
 * Returns a disposer that stops the poll.
 */
export declare function watchActiveSessionsReaderPresence(onConnect: () => void, opts?: {
    pollMs?: number;
    idleWindowMs?: number;
    nowMs?: () => number;
}): () => void;
/** Read one scope from the snapshot file (best-effort; missing/corrupt → null). */
export declare function readActiveSessionsCache(scope: ActiveCacheScope): ActiveSessionsSnapshot | null;
/**
 * Persist a snapshot for one scope (best-effort). Other scopes are preserved
 * so a local warm never drops a fleet snapshot a reader just wrote.
 */
export declare function writeActiveSessionsCache(scope: ActiveCacheScope, sessions: ActiveSession[], opts?: {
    capturedAt?: number;
    remoteDeviceCount?: number;
}): ActiveSessionsSnapshot;
/**
 * True when a snapshot is still within the freshness window. Pure — the
 * staleness invariant for live status lives here.
 */
export declare function isActiveSnapshotFresh(capturedAt: number, nowMs: number, maxAgeMs?: number): boolean;
/** Pull only the transcript-stable fields from a live row. */
export declare function pickImmutableFields(s: ActiveSession): ImmutableSessionFields;
/**
 * Transcript mtime used as the memo key. Prefer {@link ActiveSession.lastActivityMs}
 * (the transcript's last write); fall back to `startedAtMs` when no activity
 * stamp exists. Returns null when neither is known — caller must not memoize.
 */
export declare function transcriptMtimeMs(s: ActiveSession): number | null;
/**
 * Read memoized immutable fields for `(sessionId, mtimeMs)`.
 * Returns null when missing OR when the stored mtime does not match — a
 * transcript rewrite must force re-derivation of topic/label/etc.
 */
export declare function readImmutableMemo(sessionId: string, mtimeMs: number): ImmutableSessionFields | null;
/** Persist immutable fields for `(sessionId, mtimeMs)`. Live keys are stripped. */
export declare function writeImmutableMemo(sessionId: string, mtimeMs: number, fields: ImmutableSessionFields, nowMs?: number): void;
/** Drop every live-status key from a field bag (defence in depth). */
export declare function stripLiveStatusKeys<T extends Record<string, unknown>>(fields: T): T;
/**
 * True when `fields` contains no live-status key. The invariant the tests pin:
 * the immutable memo never carries status/activity/preview/etc.
 */
export declare function assertNoLiveStatusFields(fields: Record<string, unknown>): boolean;
/**
 * Write immutable memos for every session that has an id + transcript mtime.
 * Called after a live gather so the next gather (when mtime is unchanged) can
 * refill identity fields without re-deriving them.
 */
export declare function updateImmutableMemos(sessions: ReadonlyArray<ActiveSession>, nowMs?: number): void;
/**
 * Fill missing immutable fields on a live row from the memo when the transcript
 * mtime matches. Never overwrites a field the live gather already set, and
 * never copies live-status keys.
 */
/**
 * Merge the daemon-computed session summary (PHNX-3939) onto a live row from the
 * transcript-keyed `session_summaries` cache. This is the low-cost display merge:
 * one indexed by-id read, NEVER a model call — the background
 * SessionSummarizerService is the only producer. It fills only what the cache
 * holds; the `pending`/`skipped` default for a row with no summary yet is applied
 * at the watch-stream boundary ({@link resolveStreamSummaryState}), so a
 * non-stream ActiveSession consumer is unaffected. Best-effort: a DB error leaves
 * the row untouched.
 */
export declare function mergeSessionSummary(s: ActiveSession): ActiveSession;
/**
 * The `summaryState` a watch-stream row carries when the merge left it unset:
 * `pending` while the summarizer is READY (enabled AND has an endpoint, so a
 * summary is genuinely coming), `skipped` when it is off OR enabled-but-
 * unconfigured (nothing will ever populate the cache). This is the only place
 * the readiness flag is read on the read path, and it is memoized (PHNX-3939).
 */
export declare function resolveStreamSummaryState(current: import('./types.js').SummaryState | undefined, nowMs?: number): import('./types.js').SummaryState;
export declare function applyImmutableMemo(s: ActiveSession): ActiveSession;
export interface LoadLocalActiveSessionsOptions {
    /** Skip the cache and re-gather (the force-refresh path). */
    forceRefresh?: boolean;
    /** Freshness window; defaults to {@link DEFAULT_ACTIVE_CACHE_MAX_AGE_MS}. */
    maxAgeMs?: number;
    /** Clock (injectable for tests). */
    nowMs?: number;
    /**
     * Live gather. Defaults to `getActiveSessions({ localOnly: true })` so a
     * warm never dials a remote-host teammate (RUSH-2118).
     */
    gather?: () => Promise<ActiveSession[]>;
    /** Injectable cache IO for tests. */
    readCache?: typeof readActiveSessionsCache;
    writeCache?: typeof writeActiveSessionsCache;
}
export interface LoadLocalActiveSessionsResult {
    sessions: ActiveSession[];
    /** True when the row set came from the warm snapshot, not a live gather. */
    servedFromCache: boolean;
    capturedAt: number;
}
/**
 * Cache-first load of THIS host's active sessions. Default path serves the
 * daemon-warmed snapshot when fresh; `forceRefresh` or an expired snapshot
 * re-gathers and rewrites the cache.
 */
export declare function loadLocalActiveSessions(opts?: LoadLocalActiveSessionsOptions): Promise<LoadLocalActiveSessionsResult>;
export interface LoadFleetActiveSessionsOptions {
    forceRefresh?: boolean;
    maxAgeMs?: number;
    nowMs?: number;
    /** Live fleet gather (local + remote). Required — this module does not own SSH. */
    gather: () => Promise<{
        sessions: ActiveSession[];
        remoteDeviceCount: number;
        /** Peer names that went unheard on this gather (RUSH-2507). Undefined when the gather didn't compute it. */
        remoteSkipped?: string[];
        /** True when the device list itself could not be loaded — no peer was even attempted. */
        remoteDiscoveryFailed?: boolean;
    }>;
    readCache?: typeof readActiveSessionsCache;
    writeCache?: typeof writeActiveSessionsCache;
}
export interface LoadFleetActiveSessionsResult {
    sessions: ActiveSession[];
    remoteDeviceCount: number;
    servedFromCache: boolean;
    capturedAt: number;
    /**
     * Diagnostics from the gather that produced this result. Only populated on a
     * live gather (`servedFromCache: false`) — a cache hit serves a prior
     * snapshot that never persisted these fields, so they read `undefined`
     * ("not probed this call") rather than falsely claiming a clean fleet.
     */
    remoteSkipped?: string[];
    remoteDiscoveryFailed?: boolean;
}
/**
 * Cache-first load of the fleet-wide active set. A prior gather (or any surface
 * that just paid the SSH cost) leaves a fleet snapshot; subsequent menubar /
 * Factory / CLI / watchdog calls within the freshness window share it.
 */
export declare function loadFleetActiveSessions(opts: LoadFleetActiveSessionsOptions): Promise<LoadFleetActiveSessionsResult>;
/**
 * Daemon warm entry point: live-gather THIS host and write the local snapshot.
 * Never SSHes. Returns the published row count for the daemon log line.
 */
export declare function publishLocalActiveSessions(opts?: {
    gather?: () => Promise<ActiveSession[]>;
    nowMs?: number;
}): Promise<{
    sessions: ActiveSession[];
    capturedAt: number;
}>;
