import { SDK_STATE_VERSION } from "./state-version"; export type SessionIndexEventType = "host_registered" | "host_heartbeat" | "host_unregistered" | "lifecycle_started" | "lifecycle_terminal" | "session_closed" | "session_deleted" | "record_reconciled"; export type SessionActivityState = "active" | "idle"; /** Coalesced broker-owned heartbeat checkpoint (C2): state plus the observation time. */ export interface SessionActivity { state: SessionActivityState; at: number; } /** Events persisted without an OS process incarnation (v1/v2 era) are legacy provenance. */ export type SessionIdentityProvenance = "composite" | "legacy"; export type SessionTombstoneRule = "retain" | "expire"; /** * Injected retention policy (C3). The broker schedules compaction independently of * rotation; settings apply at the next scheduled compaction. `clock` drives both * retention expiry and heartbeat-freshness liveness reads. */ export interface RetentionPolicy { clock?: () => number; maxAgeMs?: number; maxRows?: number; tombstoneRule?: SessionTombstoneRule; } export interface SessionIndexObservationDeps { /** Test seam for an exact retained process-identity observation. */ retainProcess?: (pid: number) => SessionIndexProcessObservation | undefined | Promise; } export interface SessionIndexProcessObservation { readonly incarnation: string; readonly isRunning: () => boolean; } export interface SessionIndexEvent { version: typeof SDK_STATE_VERSION; indexSeq: number; type: SessionIndexEventType; sessionId: string; locator: { repo: string; stateRoot: string; }; endpointGeneration: number; pid: number; /** * OS start incarnation of `pid`, published by the host that owns that pid. A * pid is reusable, so teardown needs this binding to prove the pid is still * the same process; keeping it here, in broker-owned storage, is what lets * that proof outlive the session's own workspace. */ processIncarnation?: string; endpointMtimeMs?: number; lifecycleRequestId?: string; terminalUncertain?: boolean; /** OS process incarnation (C1); absent on legacy v1/v2 events. */ hostIncarnation?: string; /** Present on host_heartbeat checkpoints (C2). */ activity?: SessionActivity; ts: number; checksum: string; } export interface IndexedSession { sessionId: string; locator: { repo: string; stateRoot: string; }; endpointGeneration: number; pid: number; /** OS start incarnation of `pid` as published by its own host at registration. */ processIncarnation?: string; endpointMtimeMs?: number; live: boolean; indexSeq: number; lifecycleRequestId?: string; terminalUncertain?: boolean; hostIncarnation?: string; identityProvenance: SessionIdentityProvenance; activity?: SessionActivity; /** Wall-clock timestamp of the latest admitted heartbeat, when one exists. */ lastHeartbeatAt?: number; /** True when more than one unresolved authority-fencing state-root identity claims this session id. */ ambiguous: boolean; /** True when the identity's latest event is terminal (DR-1 retains stopped rows for inspection/offline tail). */ terminal: boolean; } /** A session can grant endpoint or lifecycle authority only when one authority-fencing state root claims its id. */ export declare function isSessionAuthorityEligible(session: Pick): boolean; export interface SessionList { indexSeq: number; sessions: IndexedSession[]; warnings: string[]; } export type SessionGenerationIndexStatus = { status: "current"; observedIndexSeq: number; evidenceIndexSeq: number; } | { status: "retired"; observedIndexSeq: number; evidenceIndexSeq: number; event: "host_unregistered" | "session_closed" | "session_deleted"; } | { status: "replaced"; observedIndexSeq: number; evidenceIndexSeq: number; currentGeneration: number; } | { status: "unknown"; observedIndexSeq: number; reason: "index_incomplete" | "session_not_observed" | "generation_not_observed" | "generation_reused" | "ambiguous_authority" | "proof_expired" | "reconciliation_incomplete"; }; export interface SessionIndexDiagnosis { status: "healthy" | "corrupt" | "unsupported"; validPrefixSeq: number; snapshotSeq: number; reason?: string; } export interface SessionIndexRepairResult extends SessionIndexDiagnosis { repaired: boolean; quarantinePath?: string; } /** Admission-fence rejection codes recorded in the durable index audit (C5/C4). */ export type SessionIndexAuditCode = "rejected_superseded_incarnation" | "rejected_after_tombstone"; export interface SessionIndexAuditRecord { version: typeof SDK_STATE_VERSION; code: SessionIndexAuditCode; /** indexSeq of the rejected event (unique per record; used for idempotent dedupe). */ indexSeq: number; sessionId: string; endpointGeneration: number; stateRoot: string; hostIncarnation?: string; supersededByIncarnation?: string; /** indexSeq of the superseding registration, or of the tombstone for post-delete rejections. */ supersededByIndexSeq: number; ts: number; } export declare const sessionIndexChecksum: (event: Omit) => string; /** Coalesced heartbeat checkpoint rate cap (C2): at most one per session per minute. */ export declare const SESSION_HEARTBEAT_INTERVAL_MS = 60000; export declare const DEFAULT_SESSION_RETENTION_MAX_AGE_MS: number; export declare const DEFAULT_SESSION_RETENTION_MAX_ROWS = 25000; export declare class SessionIndex { #private; constructor(agentDir: string, policy?: RetentionPolicy, observationDeps?: SessionIndexObservationDeps); open(): Promise; replay(): Promise; /** * Polling-path refresh (#4689): when both index files carry the exact change * stamp of the last completed locked pass, the in-memory projection is * already current and the locked rescan is skipped entirely — this is what * keeps an idle SessionRouter reconcile (2s cadence) from re-parsing and * re-checksumming the whole index forever. An append committed before the * stat always changes the stamp, so a miss is impossible; a change landing * after the stat is seen on the next poll, the same TOCTOU envelope a * locked read has. A corrupt suffix never takes the fast path: re-scanning * preserves the existing re-diagnosis behavior. Returns true when state was * reloaded. Authority revalidation that needs the strongest available * snapshot inside an already-locked write (append, unregister) keeps using * the exact locked paths. */ refreshIfChanged(): Promise; diagnose(): Promise; repair(): Promise; refresh(): Promise; get indexSeq(): number; append(input: Omit & Partial>): Promise; unregisterIfCurrent(expected: IndexedSession): Promise; /** Hold the canonical index lock across an authority-sensitive operation. */ withLocked(callback: () => Promise): Promise; snapshot(): Promise; /** * Broker-scheduled compaction (C3), independent of rotation size: applies the * injected retention policy to a fresh snapshot and truncates the log. */ compact(): Promise; listSessions(): SessionList; /** * Credential-free proof for one exact public endpoint generation. * * A strictly newer live generation proves replacement only when this * generation was itself observed. Otherwise terminal evidence wins only when * the retained index history positively records it for the exact generation. Missing, * ambiguous, reused, corrupt, or merely non-live state remains unknown. */ generationStatus(sessionId: string, endpointGeneration: number): Promise; /** * Broker-internal current composite-identity rows. Unlike {@link listSessions}, * this retains losing roots so an exact dead registration can be retired without * disturbing the surviving authority. */ listSessionIdentities(probedIncarnations?: ReadonlyMap): IndexedSession[]; /** * Production coalesced heartbeat checkpoint pass (C2): appends one * `host_heartbeat` per session at most once per {@link SESSION_HEARTBEAT_INTERVAL_MS}. * The pass observes liveness the same way the projection does — the host process * must be alive and, for composite identities, still carry the recorded OS process * incarnation (a reused PID is never checkpointed). Stopped, terminal, and ambiguous rows * and rows whose heartbeat is still fresh are skipped. After a broker restart, sessions whose * host survived are re-observed as live on the first pass; sessions whose host died * while the broker was down keep their stale or missing heartbeat and read as * unknown/not-live (never fresh forever). Returns the number of checkpoints written. */ checkpointLiveHeartbeats(now?: number): Promise; hostUnregisteredAfter(registration: Pick): { indexSeq: number; lifecycleRequestId?: string; } | undefined; findHostRegistration(sessionId: string, endpointGeneration: number, pid: number, lifecycleRequestId?: string): IndexedSession | undefined; hasHostRegistrationForLifecycle(sessionId: string, pid: number, lifecycleRequestId: string): boolean; }