/** * toknmtr agent — per-file byte-offset cursor. * * Persists, per transcript file, the byte offset up to which we've already parsed and * pushed events, so re-runs only emit NEW bytes appended since last time. Stored as a * single JSON file at ~/.toknmtr/cursors.json (one process at a time is assumed — there's * no file locking). * * Handles truncation/rotation: if a file's current size is smaller than the recorded * offset, the file was truncated or replaced (e.g. a session id got reused, or the file * was edited out from under us) — getOffset() resets to 0 so the whole file is reparsed. */ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { dirname, join } from 'node:path'; export const CURSOR_FILE = join(homedir(), '.toknmtr', 'cursors.json'); interface CursorEntry { offset: number; size: number; } type CursorMap = Record; let cache: CursorMap | null = null; function load(): CursorMap { if (cache) return cache; try { const parsed = JSON.parse(readFileSync(CURSOR_FILE, 'utf8')) as unknown; cache = parsed && typeof parsed === 'object' ? (parsed as CursorMap) : {}; } catch { cache = {}; } return cache; } /** Persist the in-memory cursor map to disk. Call once after a batch of setOffset() calls. */ export function save(): void { const map = load(); mkdirSync(dirname(CURSOR_FILE), { recursive: true }); writeFileSync(CURSOR_FILE, JSON.stringify(map, null, 2)); } /** * Byte offset to resume reading `filePath` from, given its current size on disk. * Returns 0 (full reparse) if there's no recorded cursor, or if the file shrank since * last run (truncated/rotated). */ export function getOffset(filePath: string, currentSize: number): number { const entry = load()[filePath]; if (!entry) return 0; if (entry.offset > currentSize) return 0; return entry.offset; } /** Record the new offset/size for `filePath` in memory. Call save() to persist. */ export function setOffset(filePath: string, offset: number, size: number): void { load()[filePath] = { offset, size }; } /** Forget a file's cursor entirely (forces a full reparse on next run). */ export function resetOffset(filePath: string): void { delete load()[filePath]; }