/**
 * Session discovery across Claude, Codex, Gemini, OpenCode, and OpenClaw.
 *
 * Performs incremental scans: each agent's session files are stat'd and compared
 * to a scan-stamp ledger in SQLite. Only files whose mtime or size changed since
 * the last run are re-parsed. All metadata is upserted into the sessions DB so
 * subsequent queries are served entirely from the cache.
 */
import type { SessionAgentId, SessionEvent, SessionMeta, TodoProgress } from './types.js';
export { machineForSessionFile } from './origin-machine.js';
import { type ScanStamp } from './db.js';
import { ToolCallCollector, type IndexedToolCall, type ToolCallCollectorSnapshot } from './tool-calls.js';
/** Options controlling which sessions to discover and how to report progress. */
export interface DiscoverOptions {
    agent?: SessionAgentId;
    /**
     * Include sessions from the user's own (unmanaged) `~/.<agent>` alongside managed
     * version homes. Defaults to true only when the agent has no managed versions, so
     * a user who has never run `agents add` sees exactly what they see today.
     */
    includeUnmanaged?: boolean;
    /** Called with how many rows the managed-only default hid, so callers can say so. */
    onHiddenUnmanaged?: (count: number) => void;
    version?: string;
    project?: string;
    all?: boolean;
    cwd?: string;
    /** Match any session whose cwd equals this or is a descendant. Overrides `cwd`. */
    cwdPrefix?: string;
    limit?: number;
    /** Internal indexed-query path: return every row in scope instead of the default page. */
    unbounded?: boolean;
    /** Filter sessions newer than this (ISO timestamp or "7d", "30d", "90d") */
    since?: string;
    /** Filter sessions older than this (ISO timestamp) */
    until?: string;
    /** Drop team-spawned sessions at the DB level, before LIMIT. */
    excludeTeamOrigin?: boolean;
    /** Keep only team-spawned sessions (used for hidden-count queries). */
    onlyTeamOrigin?: boolean;
    /** Keep only sessions from this source. */
    origin?: 'cli' | 'routine';
    /** Column to order results by (all descending): 'timestamp' (default), 'cost', or 'duration'. */
    sortBy?: 'timestamp' | 'cost' | 'duration';
    /** Trust scan-ledger rows without stat'ing every returned transcript. */
    skipExistenceCheck?: boolean;
    /** Called as each agent makes parsing progress. Totals count only files that need re-parsing (cache misses). */
    onProgress?: (progress: ScanProgress) => void;
    /** Only sessions that invoked this skill (#12) — see QueryOptions.skill in session/db.ts. */
    skill?: string;
    /** Only sessions that used a skill/command owned by this plugin (#12) — see QueryOptions.plugin. */
    plugin?: string;
    /** Exact session id — a targeted indexed lookup with no scan (RUSH-2477). */
    idExact?: string;
    /** Session id prefix — a targeted indexed lookup with no scan (RUSH-2477). */
    idPrefix?: string;
    /** On a cold miss, briefly await the scan already holding the single-flight claim. */
    waitForScan?: boolean;
}
/** Progress report emitted during incremental scanning. */
export interface ScanProgress {
    agent: SessionAgentId;
    parsed: number;
    total: number;
}
/** Lightweight metadata extracted from a Claude JSONL file during incremental scan. */
interface ClaudeSessionScan {
    timestamp?: string;
    cwd?: string;
    gitBranch?: string;
    version?: string;
    model?: string;
    topic?: string;
    firstUserMessage?: string;
    /** Harness-owned session name (`/rename` or Claude's generated `ai-title`). */
    label?: string;
    messageCount: number;
    tokenCount?: number;
    /** Real generated (output) tokens, excluding cache-read/-write context. */
    outputTokens?: number;
    /** Burn split — uncached input / cache-read / cache-write tokens (RUSH-2287). */
    inputTokens?: number;
    cacheReadTokens?: number;
    cacheWriteTokens?: number;
    /** Total USD cost accumulated from per-(model, direction) token usage. */
    costUsd?: number;
    /** USD cost with cache read/write repriced at the input rate (RUSH-2287). */
    costUsdNoCache?: number;
    /** Wall-clock duration in ms between the first and last timestamped event. */
    durationMs?: number;
    /** ISO time of the last timestamped event — the session's last activity. */
    lastActivity?: string;
    toolCallCount?: number;
    /**
     * Value of the JSONL `entrypoint` field on the first event that carries it.
     * 'cli' for real interactive sessions, 'sdk-cli' for team-spawned ones.
     */
    entrypoint?: string;
    /** Concatenated user message text, ready to hand to FTS5. */
    contentText?: string;
    /** Concatenated assistant-answer text, ready to hand to FTS5's `assistant` column. */
    assistantText?: string;
    /** Durable state signals persisted to the index by the session-state engine. */
    prUrl?: string;
    prNumber?: number;
    worktreeSlug?: string;
    ticketId?: string;
    /** Tracker refs the session CREATED (Linear create_issue / gh issue create). */
    createdTickets?: string[];
    /** Team name this session SPAWNED via `agents teams create/add` (not team-of-origin). */
    spawnedTeam?: string;
    /** Plan markdown from the last ExitPlanMode tool call (Claude sessions only). */
    plan?: string;
    todos?: TodoProgress;
    recentDirectoriesTouched?: string[];
    /** Skills invoked (#12) — see SessionMeta.skillsUsed. */
    skillsUsed?: Array<{
        name: string;
        count: number;
    }>;
    /** Fan-out left behind (RUSH-3091/3095) — see SessionMeta.subAgentCount. */
    subAgentCount?: number;
    backgroundShellCount?: number;
    /** Slash commands invoked (#12) — see SessionMeta.slashCommandsUsed. */
    slashCommandsUsed?: Array<{
        name: string;
        count: number;
    }>;
}
/** Lightweight metadata extracted from a Codex JSONL file during incremental scan. */
interface CodexSessionScan {
    sessionId?: string;
    timestamp?: string;
    cwd?: string;
    gitBranch?: string;
    version?: string;
    model?: string;
    topic?: string;
    firstUserMessage?: string;
    messageCount: number;
    tokenCount?: number;
    /** Real generated (output) tokens, excluding cache-read/-write context. */
    outputTokens?: number;
    /** Burn split — uncached input / cache-read tokens (Codex has no cache-write) (RUSH-2287). */
    inputTokens?: number;
    cacheReadTokens?: number;
    cacheWriteTokens?: number;
    costUsd?: number;
    /** USD cost with cache read repriced at the input rate (RUSH-2287). */
    costUsdNoCache?: number;
    durationMs?: number;
    lastActivity?: string;
    contentText?: string;
    assistantText?: string;
    prUrl?: string;
    prNumber?: number;
    worktreeSlug?: string;
    ticketId?: string;
    createdTickets?: string[];
    spawnedTeam?: string;
    todos?: TodoProgress;
    recentDirectoriesTouched?: string[];
}
/**
 * Discover sessions. Scans only files whose (mtime, size) have changed since
 * the last run; everything else is served from the SQLite cache.
 *
 * Only one process runs the incremental scan at a time. When many agents boot
 * simultaneously (e.g. after a restart), the first to claim the scan slot does
 * the work; the rest skip parsing entirely and serve from the DB. The claim is
 * stored in the `meta` table — crash-safe via dead-PID detection and a 2-min
 * TTL, no external lock files needed.
 */
export declare function discoverSessions(options?: DiscoverOptions): Promise<SessionMeta[]>;
/** What one incremental scan actually did. */
interface IncrementalScanResult {
    /** True when this process won the single-flight claim and ran the scan. */
    claimed: boolean;
    /** Changed transcripts parsed; zero with `claimed: true` is a successful no-op scan. */
    scanned: number;
}
/** Separate write half so daemon warming does not pay for listing or external enrichment. */
export declare function scanSessionsIncremental(options?: {
    agent?: SessionAgentId;
    onProgress?: (p: ScanProgress) => void;
}): Promise<IncrementalScanResult>;
/** Bounded wait so a wedged scan cannot hang a foreground cold-miss repair. */
export declare function waitForScanToSettle(timeoutMs?: number, pollMs?: number): Promise<boolean>;
/** Read the current SQLite snapshot without scanning or parsing transcript files. */
export declare function queryIndexedSessions(options?: DiscoverOptions, indexedOptions?: {
    resolveLinear?: boolean;
    skipExistenceCheck?: boolean;
}): Promise<SessionMeta[]>;
/**
 * Resolve locally without scanning or fleet I/O, keeping concurrent crash recovery lock-light.
 * The existence check preserves archived content while rejecting transcriptless phantoms.
 */
export declare function resolveIndexedSessionById(idQuery: string): Promise<SessionMeta[]>;
/**
 * Drop unmanaged rows for agents that HAVE managed versions.
 *
 * Scoping happens here, at query time, rather than by narrowing the scan: the index
 * stays complete, so `--unmanaged` needs no re-scan and every other consumer of the
 * DB (watchdog, the Factory watcher, `--roots`) is unaffected.
 *
 * An agent with no managed versions is left alone entirely — someone who has never
 * run `agents add` sees exactly what they saw before.
 */
export declare function scopeToManaged(sessions: SessionMeta[], agents: readonly SessionAgentId[], options?: DiscoverOptions): SessionMeta[];
/**
 * How many agents' dotfile dirs we scan at once, and the minimum spacing between
 * successive scan starts. A small bound + stagger turns a simultaneous bulk
 * multi-dotfile sweep (a behavioral-EDR file-enumeration trigger) into a trickle.
 */
export declare const DOTFILE_SCAN_CONCURRENCY = 2;
/** Run each agent's incremental scan, bounded + staggered. Order is irrelevant (each scan writes its own rows). */
export declare function scanAgentsBounded<T>(items: readonly T[], run: (item: T) => Promise<void>): Promise<void[]>;
/**
 * The machine a discovered session originated on. Cross-machine sync mirrors a
 * remote transcript to backups/<agent>/<machine>/<subdir>/… (see mirrorPath in
 * sync/agents.ts); every other transcript is a live-home file on this box. So:
 * when the path sits under the agent's backups root, the first segment below it
 * is the origin machine id; otherwise it's the local machine.
 */
/**
 * True when this transcript belongs to a version agents-cli manages — i.e. it lives
 * under a version home (or a backup mirror of one) rather than in the user's own
 * `~/.<agent>`.
 *
 * `agents sessions` scans both, which is right for indexing: the DB stays a complete
 * picture and `--unmanaged` can surface everything without a re-scan. But listing
 * *by default* is a different question. Once you have managed versions, an unmanaged
 * install's history is not really agents-cli's to show — most visibly after
 * `agents add --isolated`, where the whole point was to keep the two apart.
 */
export declare function isManagedSessionFile(filePath: string): boolean;
/**
 * Count sessions in scope without running an incremental scan. Assumes the DB
 * is already fresh (typically true because `discoverSessions` ran first this
 * turn). Uses the exact same filter shape as the discover query.
 */
export declare function countSessionsInScope(options: DiscoverOptions): number;
/**
 * Canonicalize a working directory path (follows symlinks when it is local).
 *
 * Most callers pass a cwd RECORDED in a transcript, which may name a directory
 * on another machine — a POSIX path read on a Windows host, say. `path.resolve()`
 * rebases such a path onto the current drive (`/Users/me` -> `D:\Users\me`),
 * inventing a location that never existed. So an absolute path is normalized but
 * never rebased; only a genuinely relative one resolves against the process cwd.
 *
 * `path.normalize()` still runs on every branch: it collapses `.`, `..`, and
 * duplicate separators, and folds separators on Windows. Both sides of the cwd
 * filter in `db.ts` (`cwd = ?` and `cwd LIKE ? || path.sep || '%'`) come through
 * here, so dropping that would leave a trailing slash or a `..` segment in one
 * side and match nothing.
 *
 * Realpath is attempted only for a path that is absolute in THIS platform's
 * terms. A POSIX-rooted path on Windows is drive-relative to `fs.realpathSync`,
 * which would resolve `/Users/me` against the current drive and reintroduce the
 * graft for any path that happens to exist locally.
 */
export declare function _normalizeCwdForTest(cwd?: string): string;
/**
 * Whether a query is a session id in full, rather than an id prefix or a search
 * phrase.
 *
 * Callers use this to decide that an id lookup is the ONLY admissible
 * interpretation: a complete id is unique, so when it misses there is nothing
 * left to widen to. Without the check, `sessions <uuid>` fell through to the
 * FTS content search, which tokenizes the UUID and matches every transcript that
 * merely mentions it — surfacing unrelated sessions as if they were id matches.
 *
 * The accepted shapes are the ones the index actually holds, measured over a
 * 12,507-row index: a bare UUID (11,116 rows), `session_` + UUID (1,360 — kimi
 * and rush), and `ses_` + ULID (15 — opencode). Deliberately NOT covered, so a
 * miss keeps today's search behavior rather than gaining a wrong error: routine
 * run ids (ISO timestamps, matched via `routineRunId` below) and cloud execution
 * ids, whose charset is too permissive to distinguish from a search phrase.
 */
export declare function isCompleteSessionId(query: string): boolean;
/**
 * Whether a query should be treated as a session id rather than a search phrase
 * — the one canonical id-shaped test, shared by every session-id resolver.
 *
 * True for a complete id (`isCompleteSessionId`) AND for a bare hex short-id or
 * prefix (`d3470b57`), which the complete-id check rejects. Any id-shaped query
 * resolves by id ONLY (exact -> prefix -> `findSessionsById` index) and must
 * never fall back to fuzzy content search: a short id like `d3470b57` otherwise
 * surfaces every transcript that merely MENTIONS the string (a resume prompt
 * echoes the parent id into the body of many later sessions). The hex test
 * catches the bare short-id/prefix; `isCompleteSessionId` additionally catches
 * the prefixed whole ids (`session_…`, `ses_…`) that the hex test rejects.
 */
export declare function looksLikeSessionId(query: string): boolean;
/**
 * Resolve a session by full or short ID. Accepts a pre-loaded session list
 * (fast path from discoverSessions) and falls back to a DB lookup for the
 * "I only know the id" case.
 */
export declare function resolveSessionById(sessions: SessionMeta[], idQuery: string): SessionMeta[];
/**
 * Run an FTS5 search over the DB and union hits with the given session list.
 *
 * The listing pool is a minority of the index — cwd-scoped, default-capped at
 * 50, and skipping whole classes of indexed transcript — so intersecting FTS
 * hits with it dropped grep-visible sessions that the index already found
 * (PHNX-2767: `agents sessions "tmux pane"` returned 0 while the project
 * transcripts matched). Hits already in the pool keep the caller's SessionMeta;
 * hits the pool missed are hydrated from the index so a content query returns
 * the transcript FTS matched.
 */
export declare function searchContentIndex(sessions: SessionMeta[], query: string): Map<string, SessionMeta>;
/**
 * For a list of files, stat each, compare to the DB ledger, and return only
 * the ones that need rescanning. One bulk DB query for the whole list.
 *
 * Actively running agents append to their JSONL every few seconds. Without a
 * small debounce, repeated `agents sessions` invocations stream-parse the same
 * growing transcript over and over. The cached row is good enough for a few
 * seconds; once writes settle or the debounce expires, the file is parsed once.
 */
export declare function filterChangedFiles(filePaths: string[]): Array<{
    filePath: string;
    scan: ScanStamp;
}>;
/** A path already stat'd by the walk — mtime is the raw (unfloored) fs value. */
interface PreStatEntry {
    filePath: string;
    fileMtimeMs: number;
    fileSize: number;
}
/**
 * Ledger-compare pre-stat'd entries (from the walk's own stat) without a second
 * stat. Same debounce and change-detection as filterChangedFiles; the raw
 * mtime is floored here so warm files match the ledger exactly as the stat path
 * does (Math.floor(stat.mtimeMs)).
 *
 * A file is also treated as "changed" — independent of (mtime, size) — when
 * its ledger row's `extractor_version` is behind {@link CONTENT_INDEX_VERSION}.
 * This is the lever that backfills a content-extractor improvement (e.g.
 * indexing assistant answers, not just user prompts) into every already-scanned
 * session on its next pass, without a `DELETE FROM scan_ledger` that would also
 * discard the Claude/Codex resumable parser_state.
 */
export declare function filterChangedEntries(entries: PreStatEntry[]): Array<{
    filePath: string;
    scan: ScanStamp;
}>;
export declare function shouldDeferRecentAppend(prev: ScanStamp, current: ScanStamp, nowMs: number, debounceMs?: number): boolean;
/**
 * Collect all directories to scan for an agent's sessions. Deduplicates by
 * realpath to avoid double-counting symlinked version homes.
 */
export declare function getAgentSessionDirs(agent: string, subdir: string): string[];
/** A session-agent's on-disk watch roots (every version home + backup mirror). */
interface SessionRoots {
    agent: SessionAgentId;
    /** Absolute directories that hold this agent's transcripts, existing right now. */
    dirs: string[];
}
/**
 * The directories `agents sessions` scans for each on-disk session agent,
 * resolved to what exists on this machine. Emitted by `agents sessions --roots
 * --json` so external watchers stay in lockstep with the CLI's discovery paths.
 * Agents with no directories present are omitted.
 */
export declare function getSessionRoots(): SessionRoots[];
/**
 * Build a map of Claude sessionId -> user-given label from ~/.claude/sessions/*.json.
 * Each JSON has shape { pid, sessionId, cwd, startedAt, name?, ... }. The
 * `name` field only exists if the user ran /rename in that session.
 * For sessionId collisions (re-resume of the same session), prefer the most
 * recent startedAt.
 */
export declare function buildClaudeLabelMap(): Map<string, string | null>;
/**
 * Base64url-decode a JWT and return its `email` claim, if present. Split out so
 * the decode is a single, testable step — and so it only runs when someone
 * actually reads the Codex account (see the lazy resolution below).
 */
export declare function decodeJwtEmail(idToken: string): string | undefined;
/** Test seam: how many times getCodexAccount has actually resolved (decoded) since the last reset. */
export declare function __codexAccountResolveCountForTest(): number;
/** Test seam: clear the memoized account + resolve counter so laziness can be observed from a clean slate. */
export declare function __resetCodexAccountCacheForTest(): void;
/** Parse the lines of a Codex `session_index.jsonl` into a session id -> title map. */
export declare function parseCodexThreadNameIndex(raw: string): Map<string, string>;
/**
 * Stream-parse a single Codex JSONL file to extract session metadata.
 *
 * `resolveAccount` is a lazy thunk (not a resolved string): the JWT decode it
 * performs is deferred until we know this file is a real session worth building
 * metadata for, and only then — never during the file walk / stat phase.
 */
export declare function readCodexMeta(filePath: string, resolveAccount?: () => string | undefined, currentVersion?: string, scanStamp?: ScanStamp, priorRow?: {
    parserState: string | null;
    fileMtimeMs: number;
    extractorVersion?: number | null;
}): Promise<{
    meta: SessionMeta;
    content: string;
    assistantContent: string;
    parserState?: string;
    contentText?: string;
    toolCalls?: IndexedToolCall[];
    toolIndexMode: 'replace' | 'append';
} | null>;
/**
 * Mutable accumulator for the Claude transcript reducer. One field per local
 * that {@link scanClaudeSession} previously declared inline — the reducer
 * mutates `state.*` instead of closure locals so the exact same logic can drive
 * both a full parse and a resumable incremental parse (see
 * {@link scanClaudeSessionIncremental}).
 */
interface ClaudeParseState {
    timestamp?: string;
    cwd?: string;
    gitBranch?: string;
    version?: string;
    model?: string;
    topic?: string;
    firstUserMessage?: string;
    customTitle?: string;
    aiTitle?: string;
    entrypoint?: string;
    messageCount: number;
    toolCallCount: number;
    tokenCount: number;
    outputTokens: number;
    inputTokens: number;
    cacheReadTokens: number;
    cacheWriteTokens: number;
    sawTokenCount: boolean;
    costUsd: number;
    costUsdNoCache: number;
    sawCost: boolean;
    firstTsMs?: number;
    lastTsMs?: number;
    seenAssistantIds: Set<string>;
    userTexts: string[];
    /** Assistant-answer text (#PHNX content-search), accumulated the same way as
     *  `userTexts` but indexed into session_text's own lower-weighted `assistant`
     *  column instead of `content` — see BM25_WEIGHTS. */
    assistantTexts: string[];
    sawPrCreate: boolean;
    prUrl?: string;
    prNumber?: number;
    createdTickets: Set<string>;
    pendingTicketTools: Set<string>;
    spawnedTeam?: string;
    plan?: string;
    checklistEvents: SessionEvent[];
    recentDirectoriesTouched: string[];
    toolCollector: ToolCallCollector;
    /** Skill-invocation tool_use events, held for extractSkills() at finalize (#12). */
    skillEvents: SessionEvent[];
    /** Slash-command events (user-typed <command-name> wrapper OR a SlashCommand
     *  tool_use), held for extractSlashCommands() at finalize (#12). */
    slashCommandEvents: SessionEvent[];
    /**
     * Fan-out tallies (RUSH-3091/3095). Counted as we stream rather than held as
     * events — unlike skills we need only the count, and a busy session can spawn
     * hundreds. `backgroundShells` stays undefined for a harness with no
     * background-shell concept, which must render as absence, never as 0.
     */
    subAgents: number;
    backgroundShells: number | undefined;
}
/** Zero-value accumulator for a fresh (from-byte-0) Claude parse. */
export declare function initClaudeParseState(): ClaudeParseState;
/**
 * Fold one parsed transcript line into the accumulator. This is the exact loop
 * body {@link scanClaudeSession} used to run inline — extracted verbatim,
 * mutating `state.*` in place. `parsed` is the already-`JSON.parse`d line (the
 * malformed-line skip happens in the caller, as before).
 */
export declare function applyClaudeLine(state: ClaudeParseState, parsed: any): void;
/**
 * Build the {@link ClaudeSessionScan} return object from an accumulator. This is
 * the exact return-building {@link scanClaudeSession} used to run inline.
 */
export declare function finalizeClaudeScan(state: ClaudeParseState): ClaudeSessionScan;
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessionScan>;
/**
 * SERIALIZED continuation blob persisted in `scan_ledger.parser_state`. Carries
 * everything {@link hydrateClaudeParseState} needs to resume a parse from
 * `offset` such that resuming + applying the appended lines is byte-for-byte
 * identical to a full parse of the whole file.
 *
 * `seenAssistantIds` is persisted as a size counter plus a bounded FIFO window
 * of the most-recent ids: the fallback logical id `${ts}:${seenAssistantIds.size}`
 * (see {@link applyClaudeLine}) depends on the set's *size*, so the size must be
 * exact even when the recent window is smaller than the true count.
 */
export interface ClaudeParserState {
    v: 6;
    /**
     * Fan-out tallies carried across a RESUMED parse (RUSH-3091/3095). They must
     * live in the durable blob: the resumable parser reads only new bytes, so
     * re-initialising them on hydrate would report a session's tail, not its
     * total. Adding them is why `v` moved 3 -> 4 — an older blob is rejected and
     * the session re-parses from byte 0 with correct totals.
     */
    subAgents: number;
    backgroundShells: number | undefined;
    offset: number;
    jsonlDroppingOversizedLine?: boolean;
    timestamp?: string;
    cwd?: string;
    gitBranch?: string;
    version?: string;
    model?: string;
    entrypoint?: string;
    firstTsMs?: number;
    topic?: string;
    firstUserMessage?: string;
    customTitle?: string;
    aiTitle?: string;
    plan?: string;
    lastTsMs?: number;
    messageCount: number;
    toolCallCount: number;
    tokenCount: number;
    outputTokens: number;
    inputTokens: number;
    cacheReadTokens: number;
    cacheWriteTokens: number;
    sawTokenCount: boolean;
    sawCost: boolean;
    costUsd: number;
    costUsdNoCache: number;
    seenIdsSize: number;
    seenIdsRecent: string[];
    sawPrCreate: boolean;
    prUrl?: string;
    prNumber?: number;
    pendingTicketTools: string[];
    createdTickets: string[];
    spawnedTeam?: string;
    ticketId?: string;
    contentText?: string;
    assistantContentText?: string;
    checklistEvents: SessionEvent[];
    recentDirectoriesTouched: string[];
    toolCalls: ToolCallCollectorSnapshot;
    /** #12: see ClaudeParseState.skillEvents/slashCommandEvents. */
    skillEvents: SessionEvent[];
    slashCommandEvents: SessionEvent[];
}
/**
 * Snapshot a live {@link ClaudeParseState} into its serializable form at
 * `offset` bytes consumed. Round-trips through {@link hydrateClaudeParseState}
 * so incremental replay equals a full parse.
 */
export declare function serializeClaudeParserState(state: ClaudeParseState, offset: number, jsonlDroppingOversizedLine?: boolean): ClaudeParserState;
/**
 * Rebuild a live {@link ClaudeParseState} from a persisted continuation so that
 * applying the appended lines yields the same accumulator a full parse would.
 *
 * `seenAssistantIds` is rehydrated from the recent-id FIFO window, then padded
 * with unique sentinel entries so its `.size` matches the true prior count
 * (`seenIdsSize`) — the fallback id `${ts}:${size}` must line up with the full
 * parse even when the window dropped older ids. Padding sentinels can never
 * collide with a real logical id (real ids are message ids/uuids or
 * `${ts}:${n}`; the sentinel prefix is not JSON-line-derived).
 *
 * `userTexts` is rehydrated as a single joined blob from `contentText`: only
 * `userTexts.join('\n')` (detectTicket + contentText) and `userTexts.length > 0`
 * are ever read downstream, and both are preserved by a one-element array
 * holding the joined content.
 */
export declare function hydrateClaudeParseState(prior: ClaudeParserState): ClaudeParseState;
/**
 * Resume a Claude parse from `fromOffset` bytes into the file, folding only the
 * newly-appended lines into `prior`. Returns the finalized scan, the next
 * serialized continuation, and the byte offset to resume from next time —
 * `newOffset` stops at the last `'\n'` seen so a half-written trailing record is
 * re-read (not lost) on the next append.
 *
 * NOT wired into the live scan path yet (that is B-2); {@link scanClaudeSession}
 * remains the only caller-facing entry point. This exists so the parity harness
 * can prove full === hydrate(state@k) + apply(k+1..n).
 */
export declare function scanClaudeSessionIncremental(filePath: string, fromOffset: number, prior: ClaudeParserState): Promise<{
    scan: ClaudeSessionScan;
    newState: ClaudeParserState;
    newOffset: number;
    toolCalls: IndexedToolCall[];
}>;
/**
 * Decide full-vs-incremental for one Claude file and parse it uniformly, always
 * returning a finalized scan plus the continuation to persist. Both branches run
 * through the SAME reducer (via {@link scanClaudeSessionIncremental}), so the row
 * an append produces is identical to a from-scratch full reparse by construction
 * (the B-1 parity harness proves this at the function level).
 *
 * INCREMENTAL when a prior continuation exists AND the file grew past the
 * persisted offset AND its mtime did not go backwards — an in-place append.
 * FULL (from byte 0, fresh state) otherwise: cold start (no prior), truncation /
 * rewrite (size shrank to at or below the offset), or a clock rewind / restore
 * (mtime older than the last parse). A FULL parse still produces a continuation,
 * so the file's very next append can go incremental.
 *
 * `mode` is returned so the caller (and tests) can confirm which branch ran.
 */
export declare function scanClaudeSessionResumable(filePath: string, prior: ClaudeParserState | null, currentFileMtimeMs: number, currentFileSize: number, priorFileMtimeMs?: number): Promise<{
    scan: ClaudeSessionScan;
    newState: ClaudeParserState;
    newOffset: number;
    toolCalls: IndexedToolCall[];
    mode: 'full' | 'incremental';
}>;
/** Test seam: read the (incremental, full) Claude parse counters. */
export declare function __claudeScanBranchCountsForTest(): {
    incremental: number;
    full: number;
};
/** Test seam: reset the Claude parse-branch counters to observe a scan from a clean slate. */
export declare function __resetClaudeScanBranchCountsForTest(): void;
/**
 * Live (in-memory) accumulator for a Codex parse — the mutable state
 * {@link scanCodexSession} used to hold in local `let`s, extracted so the same
 * fold ({@link applyCodexLine}) runs for both a full parse and an incremental
 * resume. Mirrors {@link ClaudeParseState}.
 */
interface CodexParseState {
    sessionId?: string;
    timestamp?: string;
    cwd?: string;
    gitBranch?: string;
    version?: string;
    model?: string;
    topic?: string;
    firstUserMessage?: string;
    messageCount: number;
    tokenCount?: number;
    lastTotalTokenUsage?: any;
    firstTsMs?: number;
    lastTsMs?: number;
    userTexts: string[];
    /** See ClaudeParseState.assistantTexts — same accumulate-and-lower-weight FTS treatment. */
    assistantTexts: string[];
    sawPrCreate: boolean;
    prUrl?: string;
    prNumber?: number;
    createdTickets: Set<string>;
    pendingTicketTools: Set<string>;
    spawnedTeam?: string;
    checklistEvents: SessionEvent[];
    recentDirectoriesTouched: string[];
    toolCollector: ToolCallCollector;
    /** #12: see ClaudeParseState.skillEvents. Empty in practice today — no
     *  verified Codex skill-invocation tool name — but wired for parity so a
     *  future confirmed tool name (or a literal 'SlashCommand' function_call)
     *  is picked up with no further plumbing. */
    skillEvents: SessionEvent[];
    slashCommandEvents: SessionEvent[];
}
/** Zero-value accumulator for a fresh (from-byte-0) Codex parse. */
export declare function initCodexParseState(): CodexParseState;
/**
 * SERIALIZED continuation blob persisted in `scan_ledger.parser_state` for a
 * Codex rollout. Carries everything {@link hydrateCodexParseState} needs to
 * resume a parse from `offset` such that resuming + applying the appended lines
 * is byte-for-byte identical to a full parse of the whole file.
 *
 * Unlike Claude, Codex has NO per-message dedup set, so `messageCount` is a
 * plain additive base with no recent-id window to persist. The `lastTotalTokenUsage`
 * object is round-tripped whole so the last-wins cost/output-token pricing at
 * finalize is identical after a resume.
 */
export interface CodexParserState {
    v: 4;
    offset: number;
    jsonlDroppingOversizedLine?: boolean;
    sessionId?: string;
    timestamp?: string;
    cwd?: string;
    gitBranch?: string;
    version?: string;
    model?: string;
    topic?: string;
    firstUserMessage?: string;
    messageCount: number;
    tokenCount?: number;
    lastTotalTokenUsage?: any;
    firstTsMs?: number;
    lastTsMs?: number;
    sawPrCreate: boolean;
    prUrl?: string;
    prNumber?: number;
    pendingTicketTools: string[];
    createdTickets: string[];
    spawnedTeam?: string;
    ticketId?: string;
    contentText?: string;
    assistantContentText?: string;
    checklistEvents: SessionEvent[];
    recentDirectoriesTouched: string[];
    toolCalls: ToolCallCollectorSnapshot;
}
/**
 * Snapshot a live {@link CodexParseState} into its serializable form at `offset`
 * bytes consumed. Round-trips through {@link hydrateCodexParseState} so
 * incremental replay equals a full parse.
 */
export declare function serializeCodexParserState(state: CodexParseState, offset: number, jsonlDroppingOversizedLine?: boolean): CodexParserState;
/**
 * Resume a Codex parse from `fromOffset` bytes into the file, folding only the
 * newly-appended lines into `prior`. Returns the finalized scan, the next
 * serialized continuation, and the byte offset to resume from next time.
 *
 * Same trailing-line discipline as {@link scanClaudeSessionIncremental}: apply
 * ONLY the run of newline-terminated lines (slice at the last `'\n'`), and set
 * `newOffset = fromOffset + consumedBytes`. An ordinary tail after the last
 * `'\n'` is deferred; an oversized one advances under a persisted discard bit.
 * Codex `messageCount` is additive with
 * NO dedup, so re-reading a still-unterminated complete line would double-count
 * it; deferring prevents that (the bug class prix-cloud caught for Claude).
 */
export declare function scanCodexSessionIncremental(filePath: string, fromOffset: number, prior: CodexParserState): Promise<{
    scan: CodexSessionScan;
    newState: CodexParserState;
    newOffset: number;
    toolCalls: IndexedToolCall[];
}>;
/**
 * Decide full-vs-incremental for one Codex rollout and parse it uniformly,
 * always returning a finalized scan plus the continuation to persist. Both
 * branches run through the SAME reducer (via {@link scanCodexSessionIncremental}),
 * so an append produces a row identical to a from-scratch full reparse by
 * construction. INCREMENTAL when a prior continuation exists AND the file grew
 * past the persisted offset AND its mtime did not go backwards; FULL (from byte
 * 0, fresh state) otherwise (cold start, truncation/rewrite, clock rewind).
 */
export declare function scanCodexSessionResumable(filePath: string, prior: CodexParserState | null, currentFileMtimeMs: number, currentFileSize: number, priorFileMtimeMs?: number): Promise<{
    scan: CodexSessionScan;
    newState: CodexParserState;
    newOffset: number;
    toolCalls: IndexedToolCall[];
    mode: 'full' | 'incremental';
}>;
/** Test seam: read the (incremental, full) Codex parse counters. */
export declare function __codexScanBranchCountsForTest(): {
    incremental: number;
    full: number;
};
/** Test seam: reset the Codex parse-branch counters to observe a scan from a clean slate. */
export declare function __resetCodexScanBranchCountsForTest(): void;
/** Extract the version number from a managed versions/<agent>/<version>/... path under either repo. */
export declare function extractVersionFromManagedPath(agent: SessionAgentId, sourcePath?: string): string | undefined;
/** Parse one Cursor transcript and enrich it with the matching chat meta.json. */
export declare function readCursorMeta(filePath: string, currentVersion?: string): {
    meta: SessionMeta;
    content: string;
    assistantContent: string;
    events: SessionEvent[];
} | null;
/** Parse a single Kimi session state.json file to extract session metadata. */
export declare function readKimiMeta(filePath: string, priorRow?: {
    parserState: string | null;
}): {
    meta: SessionMeta;
    content: string;
    parserState?: string;
} | null;
/**
 * Kimi wire metrics are pure additive counters (messageCount, tokenCount,
 * outputTokens) with NO straddle/dedup state, so the continuation is just those
 * three bases plus the byte `offset` already consumed from wire.jsonl. Resuming
 * from `offset` + adding the appended tail's deltas equals a full parse.
 */
export interface KimiParserState {
    v: 2;
    offset: number;
    messageCount: number;
    tokenCount: number;
    outputTokens: number;
    firstUserMessage?: string;
}
/**
 * Incrementally parse Kimi's wire.jsonl for message-count and token counters,
 * resuming from a persisted continuation instead of re-reading from byte 0 every
 * scan. Returns the finalized counters and the next {@link KimiParserState} to
 * persist (offset + the three counter bases).
 *
 * Same trailing-line discipline as {@link scanClaudeSessionIncremental}: read
 * only the appended byte range from `prior.offset`, apply ONLY the run of
 * newline-terminated lines (slice at the last `'\n'`), and advance the offset to
 * `prior.offset + consumedBytes`. A complete-but-not-yet-terminated last record
 * is DEFERRED to the next pass; because these counters are additive with no
 * dedup, re-reading such a line would double-count it. FULL parse from byte 0
 * (fresh counters) when there is no prior OR the file shrank below the stored
 * offset (truncation/rewrite).
 */
export declare function parseKimiWireMetricsIncremental(sessionDir: string, prior: KimiParserState | null): {
    messageCount: number;
    tokenCount: number;
    outputTokens: number;
    newState: KimiParserState;
};
/** Parse a single Grok session summary.json into session metadata. */
export declare function readGrokMeta(filePath: string, currentVersion?: string): {
    meta: SessionMeta;
    content: string;
} | null;
export { parseTimeFilter } from './relative-time.js';
