import { type LineSpan } from "../../utils/fileIdentity"; export interface ConversationWatcherEvents { /** Fires once per new newline-terminated line appended to a watched file. */ onNewLine?: (filePath: string, line: string) => void; /** * Fires once per chokidar read with ALL new lines from that read, batched. * When set, it REPLACES the per-line onNewLine dispatch for that file — * callers pick one. Lets a burst of appended lines collapse into a single * downstream cache write + WebSocket broadcast. */ onNewLines?: (filePath: string, lines: string[]) => void; /** * Like onNewLines but also carries each line's absolute byte span in the * file (for the offset index). Fires ALONGSIDE onNewLines/onNewLine (it does * not replace them) so the cache tail write and the index extend can both * consume the same read. `readFrom` is the absolute byte offset the read * started at; `spans` are complete lines only (a torn trailing line is held * for the next read). */ onNewLineSpans?: (filePath: string, spans: LineSpan[], readFrom: number, endOffset: number) => void; /** Fires when chokidar reports an add/change/unlink at the directory level. */ onConversationChanged?: (filePath: string) => void | Promise; /** Fires when a tailed file is deleted (per-file watcher unlink event). */ onFileDeleted?: (filePath: string) => void; /** * Fires when a tailed file shrank below our read offset (in-place truncation * or replacement by a shorter file). The tail has already reset to byte 0; * the consumer must discard any byte-offset index built for the old content, * which no longer describes this file. */ onTruncated?: (filePath: string) => void; /** Reported errors per file. */ onError?: (filePath: string, error: Error) => void; } /** * Chokidar-backed replacement for src/file-watcher.ts. * * - watch(filePath) → tail a single JSONL file, emitting onNewLine * for each appended line. * - watchDirectory(dir) → mark cache dirty on add/change/unlink events * for any file inside the directory. * * Per the refactor plan, file watching is an OPTIMIZATION; correctness * still relies on refresh=1 / the latest HDD conversation id check. */ export declare class ConversationWatcher { private files; private directories; private onNewLine; private onNewLines; private onNewLineSpans; private onConversationChanged; private onFileDeleted; private onTruncated; private onError; constructor(events?: ConversationWatcherEvents); watch(filePath: string): void; unwatch(filePath: string): void; /** * Re-drive the tail read for a file that's already being tailed. A per-file * chokidar handle can die silently (fs.watch stops firing after inode churn) * while the coarser directory watcher keeps reporting changes — calling this * from the directory-event path makes the tail self-healing. Reads are * offset-based and coalesced, so a redundant poke after a normal change * event is a cheap stat + no-op. Returns false for untailed paths. */ poke(filePath: string): boolean; /** * Watch a directory of conversation JSONL files. Fires * onConversationChanged for any add/change/unlink event so the caller * can mark the cache dirty without scanning everything immediately. * * **This costs one OS watch handle per file under `directory`, not one per * directory.** chokidar recurses the tree and registers a separate fs.watch * per entry, because a directory watch alone does not report writes to files * inside it — and per-file `change` events are exactly what the caller needs * (they drive poke()'s tail self-heal and the external-tail attach). So the * handle count tracks the size of the conversation corpus on disk, not the * number of live sessions, and it does not shrink until transcripts are * deleted. `ignoreInitial` suppresses the startup *events*, not the walk. * * Measured 2026-08-09 on the live macOS instance: 2131 open .jsonl fds * against 2133 files under the watched roots — 1:1, ~88% of all fds on the * process, at 2.0% of that box's 122 880 per-process ceiling. Comfortable * there. **Linux is the tight one**: these are inotify watches billed to the * per-user `max_user_watches`, which can be 8192 and is shared with every * other watcher the user runs. Exhaustion surfaces as ENOSPC on the `error` * event — which is why server.ts wires onError rather than leaving it unset. * * Before trading handles for a bound here, note the regression it invites: a * conversation excluded from the walk (by age or by an LRU cap) is one whose * external appends produce no event at all, and that failure is silent. */ watchDirectory(directory: string): void; unwatchDirectory(directory: string): void; dispose(): void; private readNewLines; } //# sourceMappingURL=conversationWatcher.d.ts.map