import { type HqEventEnvelope, type HqKanbanSnapshotPayload, type HqSnapshot } from './protocol.js'; export interface HqEventLogOptions { dataDir: string; maxLines?: number; rotateKeep?: number; } /** * Append-only JSONL event log. Every received event envelope is appended to * `events.jsonl`; when the file exceeds `maxLines` it is rotated under a file * lock to keep only the most recent `rotateKeep` lines. * * Writes are serialized through a FIFO chain so concurrent appends never * interleave. All operations are best-effort: a rejected append resolves * (never rejects) and the caller's `await` never breaks the server loop. */ export declare class HqEventLog { private readonly filePath; private readonly maxLines; private readonly rotateKeep; private readonly writer; private lineCount; private counted; private hydration; constructor(opts: HqEventLogOptions); /** Append an event envelope as one JSON line. Best-effort, never rejects. */ append(event: HqEventEnvelope): void; /** Resolves once all queued appends have settled. For tests. */ drain(): Promise; private appendInternal; private rotate; private countLines; private ensureLineCount; /** * Read the most recent `limit` events, optionally filtered by envelope * `type`. Newest first. Returns `[]` if the file doesn't exist yet. * * Reads are best-effort against a concurrent appender: the file size is * sampled once at entry and may return one duplicate or miss one tail * line if another process appends during the scan. When a `typeFilter` * is set and matches are sparse, the scan walks the whole file in * bounded windows but is still guaranteed to reach every line. */ recent(limit: number, typeFilter?: string): Promise; /** Initialize the line count cache from disk (call once at boot). */ hydrate(): Promise; } export interface HqSnapshotStoreOptions { dataDir: string; } /** * Atomic checkpoint of the latest snapshot, written to `snapshot.json`. * The HQ server writes on every debounced broadcast and reads on boot to * re-seed its in-memory state. Best-effort, never rejects. */ export declare class HqSnapshotStore { private readonly filePath; private readonly writer; constructor(opts: HqSnapshotStoreOptions); /** Persist a snapshot. Best-effort, never rejects. */ save(snapshot: HqSnapshot): void; /** Resolves once all queued saves have settled. For tests. */ drain(): Promise; /** Read the last persisted snapshot, or `null` if none. */ load(): Promise; } export declare class HqKanbanStore { private readonly dirPath; private readonly cache; private readonly writers; private readonly mergeChains; constructor(dataDir: string); load(projectId: string): Promise; /** Merge by revision, then timestamp. HQ never lets a stale writer win. */ merge(incoming: HqKanbanSnapshotPayload): Promise; private mergeNow; drain(): Promise; private writer; private filePath; } export interface HqTimeseriesSample { /** Bucket start (epoch ms, floored to the bucket width). */ ts: number; /** Total cost (USD) accumulated in this bucket. */ costUsd: number; /** Total input tokens in this bucket. */ inputTokens: number; /** Total output tokens in this bucket. */ outputTokens: number; /** Number of tool executions in this bucket. */ toolCalls: number; /** Snapshot of active agents at bucket close (last-write per bucket). */ activeAgents?: number; /** Per-dimension cost/token breakdowns for richer trend charts. */ byModel?: Record; byProvider?: Record; } /** * One row of a per-dimension breakdown inside a {@link HqTimeseriesSample} * (e.g. the cost + tokens attributed to a single model or provider within the * bucket). Sums of these across a dimension equal the bucket's totals when * every cost signal carried that dimension. */ export interface HqTimeseriesBreakdownEntry { costUsd: number; inputTokens: number; outputTokens: number; cacheRead?: number | undefined; cacheWrite?: number | undefined; } export interface HqTimeseriesStoreOptions { dataDir: string; /** Bucket width in ms. Default 5 minutes. */ bucketMs?: number; /** How many buckets to retain. Default 2016 (1 week of 5-min buckets). */ maxBuckets?: number; } /** * Time-bucketed cost + activity samples for trend charts. Each {@link record} * call folds a cost/tool signal into the current bucket; {@link flush} writes * the accumulated buckets to `timeseries.jsonl` (append under lock) and prunes * to `maxBuckets`. * * The store keeps an in-memory ring of buckets for cheap reads; {@link load} * rehydrates them on boot. */ export declare class HqTimeseriesStore { private readonly filePath; private readonly bucketMs; private readonly maxBuckets; private readonly buckets; private readonly dirtyVersions; private readonly writer; private nextDirtyVersion; private diskLineCount; private loaded; private loadPromise; constructor(opts: HqTimeseriesStoreOptions); private bucketStart; /** Fold a cost/tool signal into the current bucket. Best-effort. */ record(signal: { ts?: number; costUsd?: number; inputTokens?: number; outputTokens?: number; toolCalls?: number; activeAgents?: number; /** When present, this signal is also attributed to a model dimension. */ model?: string; /** When present, this signal is also attributed to a provider dimension. */ provider?: string; cacheRead?: number; cacheWrite?: number; }): void; /** * Persist accumulated buckets to disk (append under lock), prune to maxBuckets. * * Note: this dispatcher coalesces — if `flush()` is called multiple times * before the previous flush settles, only the most-recent snapshot is * written. The dropped snapshots came from the same in-memory `buckets` * map, so their data is folded into the surviving snapshot rather than * lost. Callers that need every intermediate window on disk should use a * BestEffortBatchQueue instead of BestEffortLatestQueue here. */ flush(): void; /** Resolves once all queued flushes have settled. For tests. */ drain(): Promise; private flushInternal; /** Read buckets within `[since, now]`, oldest-first. */ read(sinceMs?: number): Promise; /** Rehydrate buckets from disk (deduped, latest-per-bucket wins). */ load(): Promise; private loadInternal; } /** * A minimal append-only JSONL log used for records that don't need rotation * compaction — command-audit entries and fired alerts. Each record is one * JSON line appended under a FIFO write chain; {@link readAll} parses them * oldest-first. Best-effort: a rejected append resolves (never rejects) so the * HQ server hot path is never broken. */ export interface HqSimpleLogOptions { dataDir: string; filename: string; maxLines?: number; rotateKeep?: number; readLimit?: number; } export declare class HqSimpleLog { private readonly filePath; private readonly maxLines; private readonly rotateKeep; private readonly readLimit; private readonly writer; private lineCount; private counted; private hydration; constructor(opts: HqSimpleLogOptions); /** Append one record as a JSON line. Best-effort, never rejects. */ append(record: T): void; /** Resolves once all queued appends have settled. For tests. */ drain(): Promise; private appendInternal; private ensureLineCount; /** Read all records oldest-first. Returns `[]` if the file doesn't exist. */ readAll(): Promise; } export interface HqPersistence { eventLog: HqEventLog; snapshotStore: HqSnapshotStore; timeseries: HqTimeseriesStore; kanban: HqKanbanStore; commandLog: HqSimpleLog; alertLog: HqSimpleLog; } export declare function createHqPersistence(dataDir: string): HqPersistence; //# sourceMappingURL=persistence.d.ts.map